/vendor/jeremeamia/SuperClosure/src/SerializableClosure.php

https://gitlab.com/ealexis.t/trends · PHP · 217 lines · 85 code · 17 blank · 115 comment · 6 complexity · 5a58d4620d3cb9afb06552dfa54a99bd MD5 · raw file

  1. <?php namespace SuperClosure;
  2. use Closure;
  3. use SuperClosure\Exception\ClosureUnserializationException;
  4. /**
  5. * This class acts as a wrapper for a closure, and allows it to be serialized.
  6. *
  7. * With the combined power of the Reflection API, code parsing, and the infamous
  8. * `eval()` function, you can serialize a closure, unserialize it somewhere
  9. * else (even a different PHP process), and execute it.
  10. */
  11. class SerializableClosure implements \Serializable
  12. {
  13. /**
  14. * The closure being wrapped for serialization.
  15. *
  16. * @var Closure
  17. */
  18. private $closure;
  19. /**
  20. * The serializer doing the serialization work.
  21. *
  22. * @var SerializerInterface
  23. */
  24. private $serializer;
  25. /**
  26. * The data from unserialization.
  27. *
  28. * @var array
  29. */
  30. private $data;
  31. /**
  32. * Create a new serializable closure instance.
  33. *
  34. * @param Closure $closure
  35. * @param SerializerInterface|null $serializer
  36. */
  37. public function __construct(
  38. \Closure $closure,
  39. SerializerInterface $serializer = null
  40. ) {
  41. $this->closure = $closure;
  42. $this->serializer = $serializer ?: new Serializer;
  43. }
  44. /**
  45. * Return the original closure object.
  46. *
  47. * @return Closure
  48. */
  49. public function getClosure()
  50. {
  51. return $this->closure;
  52. }
  53. /**
  54. * Delegates the closure invocation to the actual closure object.
  55. *
  56. * Important Notes:
  57. *
  58. * - `ReflectionFunction::invokeArgs()` should not be used here, because it
  59. * does not work with closure bindings.
  60. * - Args passed-by-reference lose their references when proxied through
  61. * `__invoke()`. This is an unfortunate, but understandable, limitation
  62. * of PHP that will probably never change.
  63. *
  64. * @return mixed
  65. */
  66. public function __invoke()
  67. {
  68. return call_user_func_array($this->closure, func_get_args());
  69. }
  70. /**
  71. * Clones the SerializableClosure with a new bound object and class scope.
  72. *
  73. * The method is essentially a wrapped proxy to the Closure::bindTo method.
  74. *
  75. * @param mixed $newthis The object to which the closure should be bound,
  76. * or NULL for the closure to be unbound.
  77. * @param mixed $newscope The class scope to which the closure is to be
  78. * associated, or 'static' to keep the current one.
  79. * If an object is given, the type of the object will
  80. * be used instead. This determines the visibility of
  81. * protected and private methods of the bound object.
  82. *
  83. * @return SerializableClosure
  84. * @link http://www.php.net/manual/en/closure.bindto.php
  85. */
  86. public function bindTo($newthis, $newscope = 'static')
  87. {
  88. return new self(
  89. $this->closure->bindTo($newthis, $newscope),
  90. $this->serializer
  91. );
  92. }
  93. /**
  94. * Serializes the code, context, and binding of the closure.
  95. *
  96. * @return string|null
  97. * @link http://php.net/manual/en/serializable.serialize.php
  98. */
  99. public function serialize()
  100. {
  101. try {
  102. $this->data = $this->data ?: $this->serializer->getData($this->closure, true);
  103. return serialize($this->data);
  104. } catch (\Exception $e) {
  105. trigger_error(
  106. 'Serialization of closure failed: ' . $e->getMessage(),
  107. E_USER_NOTICE
  108. );
  109. // Note: The serialize() method of Serializable must return a string
  110. // or null and cannot throw exceptions.
  111. return null;
  112. }
  113. }
  114. /**
  115. * Unserializes the closure.
  116. *
  117. * Unserializes the closure's data and recreates the closure using a
  118. * simulation of its original context. The used variables (context) are
  119. * extracted into a fresh scope prior to redefining the closure. The
  120. * closure is also rebound to its former object and scope.
  121. *
  122. * @param string $serialized
  123. *
  124. * @throws ClosureUnserializationException
  125. * @link http://php.net/manual/en/serializable.unserialize.php
  126. */
  127. public function unserialize($serialized)
  128. {
  129. // Unserialize the closure data and reconstruct the closure object.
  130. $this->data = unserialize($serialized);
  131. $this->closure = __reconstruct_closure($this->data);
  132. // Throw an exception if the closure could not be reconstructed.
  133. if (!$this->closure instanceof Closure) {
  134. throw new ClosureUnserializationException(
  135. 'The closure is corrupted and cannot be unserialized.'
  136. );
  137. }
  138. // Rebind the closure to its former binding and scope.
  139. if ($this->data['binding'] || $this->data['isStatic']) {
  140. $this->closure = $this->closure->bindTo(
  141. $this->data['binding'],
  142. $this->data['scope']
  143. );
  144. }
  145. }
  146. /**
  147. * Returns closure data for `var_dump()`.
  148. *
  149. * @return array
  150. */
  151. public function __debugInfo()
  152. {
  153. return $this->data ?: $this->serializer->getData($this->closure, true);
  154. }
  155. }
  156. /**
  157. * Reconstruct a closure.
  158. *
  159. * HERE BE DRAGONS!
  160. *
  161. * The infamous `eval()` is used in this method, along with the error
  162. * suppression operator, and variable variables (i.e., double dollar signs) to
  163. * perform the unserialization logic. I'm sorry, world!
  164. *
  165. * This is also done inside a plain function instead of a method so that the
  166. * binding and scope of the closure are null.
  167. *
  168. * @param array $__data Unserialized closure data.
  169. *
  170. * @return Closure|null
  171. * @internal
  172. */
  173. function __reconstruct_closure(array $__data)
  174. {
  175. // Simulate the original context the closure was created in.
  176. foreach ($__data['context'] as $__var_name => &$__value) {
  177. if ($__value instanceof SerializableClosure) {
  178. // Unbox any SerializableClosures in the context.
  179. $__value = $__value->getClosure();
  180. } elseif ($__value === Serializer::RECURSION) {
  181. // Track recursive references (there should only be one).
  182. $__recursive_reference = $__var_name;
  183. }
  184. // Import the variable into this scope.
  185. ${$__var_name} = $__value;
  186. }
  187. // Evaluate the code to recreate the closure.
  188. try {
  189. if (isset($__recursive_reference)) {
  190. // Special handling for recursive closures.
  191. @eval("\${$__recursive_reference} = {$__data['code']};");
  192. $__closure = ${$__recursive_reference};
  193. } else {
  194. @eval("\$__closure = {$__data['code']};");
  195. }
  196. } catch (\ParseError $e) {
  197. // Discard the parse error.
  198. }
  199. return isset($__closure) ? $__closure : null;
  200. }