Vendor lock

This commit is contained in:
2023-06-16 02:08:47 +00:00
parent 7933e70e90
commit 3351b92dd6
4099 changed files with 345789 additions and 0 deletions

View File

@ -0,0 +1,18 @@
# MIT License
Copyright (c) 2010-2015 Jeremy Lindblom
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.**

View File

@ -0,0 +1,39 @@
{
"name": "jeremeamia/superclosure",
"type": "library",
"description": "Serialize Closure objects, including their context and binding",
"keywords": ["closure", "serialize", "serializable", "function", "parser", "tokenizer", "lambda"],
"homepage": "https://github.com/jeremeamia/super_closure",
"license": "MIT",
"authors": [
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia",
"role": "Developer"
}
],
"require": {
"php": ">=5.4",
"nikic/php-parser": "^1.2|^2.0|^3.0|^4.0",
"symfony/polyfill-php56": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0|^5.0"
},
"autoload": {
"psr-4": {
"SuperClosure\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"SuperClosure\\Test\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
}
}
}

View File

@ -0,0 +1,148 @@
<?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\Node\Expr\Variable as VariableNode;
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++;
}
if ($node->var instanceof VariableNode) {
// For PHP-Parser >=4.0
return $node->var->name;
} else {
// For PHP-Parser <4.0
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

@ -0,0 +1,68 @@
<?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

@ -0,0 +1,70 @@
<?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

@ -0,0 +1,118 @@
<?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

@ -0,0 +1,120 @@
<?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 !== null
? $node->name->toString()
: null;
$this->location['namespace'] = $namespace;
}
if ($node instanceof TraitNode) {
$this->location['trait'] = (string) $node->name;
$this->location['class'] = null;
} elseif ($node instanceof ClassNode) {
$this->location['class'] = (string) $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

@ -0,0 +1,50 @@
<?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') ?: 0);
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

@ -0,0 +1,27 @@
<?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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,217 @@
<?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

@ -0,0 +1,221 @@
<?php namespace SuperClosure;
use SuperClosure\Analyzer\AstAnalyzer as DefaultAnalyzer;
use SuperClosure\Analyzer\ClosureAnalyzer;
use SuperClosure\Exception\ClosureSerializationException;
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 ($serialized === null) {
throw new ClosureSerializationException(
'The closure could not be serialized.'
);
}
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

@ -0,0 +1,45 @@
<?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);
}