├── src ├── Contracts │ ├── Serializable.php │ └── Signer.php ├── Exceptions │ ├── PhpVersionNotSupportedException.php │ ├── MissingSecretKeyException.php │ └── InvalidSignatureException.php ├── Support │ ├── ClosureScope.php │ ├── SelfReference.php │ ├── ClosureStream.php │ └── ReflectionClosure.php ├── Signers │ └── Hmac.php ├── UnsignedSerializableClosure.php ├── Serializers │ ├── Signed.php │ └── Native.php └── SerializableClosure.php ├── LICENSE.md ├── composer.json └── README.md /src/Contracts/Serializable.php: -------------------------------------------------------------------------------- 1 | hash = $hash; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidSignatureException.php: -------------------------------------------------------------------------------- 1 | secret = $secret; 25 | } 26 | 27 | /** 28 | * Sign the given serializable. 29 | * 30 | * @param string $serialized 31 | * @return array 32 | */ 33 | public function sign($serialized) 34 | { 35 | return [ 36 | 'serializable' => $serialized, 37 | 'hash' => base64_encode(hash_hmac('sha256', $serialized, $this->secret, true)), 38 | ]; 39 | } 40 | 41 | /** 42 | * Verify the given signature. 43 | * 44 | * @param array{serializable: string, hash: string} $signature 45 | * @return bool 46 | */ 47 | public function verify($signature) 48 | { 49 | return hash_equals(base64_encode( 50 | hash_hmac('sha256', $signature['serializable'], $this->secret, true) 51 | ), $signature['hash']); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/serializable-closure", 3 | "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", 4 | "keywords": ["laravel", "Serializable", "closure"], 5 | "license": "MIT", 6 | "support": { 7 | "issues": "https://github.com/laravel/serializable-closure/issues", 8 | "source": "https://github.com/laravel/serializable-closure" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | }, 15 | { 16 | "name": "Nuno Maduro", 17 | "email": "nuno@laravel.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1" 22 | }, 23 | "require-dev": { 24 | "illuminate/support": "^10.0|^11.0|^12.0", 25 | "nesbot/carbon": "^2.67|^3.0", 26 | "pestphp/pest": "^2.36|^3.0|^4.0", 27 | "phpstan/phpstan": "^2.0", 28 | "symfony/var-dumper": "^6.2.0|^7.0.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Laravel\\SerializableClosure\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Tests\\": "tests/" 38 | } 39 | }, 40 | "extra": { 41 | "branch-alias": { 42 | "dev-master": "2.x-dev" 43 | } 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "pestphp/pest-plugin": true 48 | }, 49 | "audit": { 50 | "block-insecure": false 51 | }, 52 | "sort-packages": true 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true 56 | } 57 | -------------------------------------------------------------------------------- /src/UnsignedSerializableClosure.php: -------------------------------------------------------------------------------- 1 | serializable = new Serializers\Native($closure); 25 | } 26 | 27 | /** 28 | * Resolve the closure with the given arguments. 29 | * 30 | * @return mixed 31 | */ 32 | public function __invoke() 33 | { 34 | return call_user_func_array($this->serializable, func_get_args()); 35 | } 36 | 37 | /** 38 | * Gets the closure. 39 | * 40 | * @return \Closure 41 | */ 42 | public function getClosure() 43 | { 44 | return $this->serializable->getClosure(); 45 | } 46 | 47 | /** 48 | * Get the serializable representation of the closure. 49 | * 50 | * @return array{serializable: \Laravel\SerializableClosure\Contracts\Serializable} 51 | */ 52 | public function __serialize() 53 | { 54 | return [ 55 | 'serializable' => $this->serializable, 56 | ]; 57 | } 58 | 59 | /** 60 | * Restore the closure after serialization. 61 | * 62 | * @param array{serializable: \Laravel\SerializableClosure\Contracts\Serializable} $data 63 | * @return void 64 | */ 65 | public function __unserialize($data) 66 | { 67 | $this->serializable = $data['serializable']; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Serializers/Signed.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 34 | } 35 | 36 | /** 37 | * Resolve the closure with the given arguments. 38 | * 39 | * @return mixed 40 | */ 41 | public function __invoke() 42 | { 43 | return call_user_func_array($this->closure, func_get_args()); 44 | } 45 | 46 | /** 47 | * Gets the closure. 48 | * 49 | * @return \Closure 50 | */ 51 | public function getClosure() 52 | { 53 | return $this->closure; 54 | } 55 | 56 | /** 57 | * Get the serializable representation of the closure. 58 | * 59 | * @return array 60 | */ 61 | public function __serialize() 62 | { 63 | if (! static::$signer) { 64 | throw new MissingSecretKeyException(); 65 | } 66 | 67 | return static::$signer->sign( 68 | serialize(new Native($this->closure)) 69 | ); 70 | } 71 | 72 | /** 73 | * Restore the closure after serialization. 74 | * 75 | * @param array{serializable: string, hash: string} $signature 76 | * @return void 77 | * 78 | * @throws \Laravel\SerializableClosure\Exceptions\InvalidSignatureException 79 | */ 80 | public function __unserialize($signature) 81 | { 82 | if (static::$signer && ! static::$signer->verify($signature)) { 83 | throw new InvalidSignatureException(); 84 | } 85 | 86 | /** @var \Laravel\SerializableClosure\Contracts\Serializable $serializable */ 87 | $serializable = unserialize($signature['serializable']); 88 | 89 | $this->closure = $serializable->getClosure(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serializable Closure 2 | 3 | 4 | Build Status 5 | 6 | 7 | Total Downloads 8 | 9 | 10 | Latest Stable Version 11 | 12 | 13 | License 14 | 15 | 16 | ## Introduction 17 | 18 | > This project is a fork of the excellent [opis/closure: 3.x](https://github.com/opis/closure) package. At Laravel, we decided to fork this package as the upcoming version [4.x](https://github.com/opis/closure) is a complete rewrite on top of the [FFI extension](https://www.php.net/manual/en/book.ffi.php). As Laravel is a web framework, and FFI is not enabled by default in web requests, this fork allows us to keep using the `3.x` series while adding support for new PHP versions. 19 | 20 | Laravel Serializable Closure provides an easy and secure way to **serialize closures in PHP**. 21 | 22 | ## Official Documentation 23 | 24 | ### Installation 25 | 26 | > **Requires [PHP 7.4+](https://php.net/releases/)** 27 | 28 | First, install Laravel Serializable Closure via the [Composer](https://getcomposer.org/) package manager: 29 | 30 | ```bash 31 | composer require laravel/serializable-closure 32 | ``` 33 | 34 | ### Usage 35 | 36 | You may serialize a closure this way: 37 | 38 | ```php 39 | use Laravel\SerializableClosure\SerializableClosure; 40 | 41 | $closure = fn () => 'james'; 42 | 43 | // Recommended 44 | SerializableClosure::setSecretKey('secret'); 45 | 46 | $serialized = serialize(new SerializableClosure($closure)); 47 | $closure = unserialize($serialized)->getClosure(); 48 | 49 | echo $closure(); // james; 50 | ``` 51 | 52 | ### Caveats 53 | 54 | * Anonymous classes cannot be created within closures. 55 | * Attributes cannot be used within closures. 56 | * Serializing closures on REPL environments like Laravel Tinker is not supported. 57 | * Serializing closures that reference objects with readonly properties is not supported. 58 | 59 | ## Contributing 60 | 61 | Thank you for considering contributing to Serializable Closure! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). 62 | 63 | ## Code of Conduct 64 | 65 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 66 | 67 | ## Security Vulnerabilities 68 | 69 | Please review [our security policy](https://github.com/laravel/serializable-closure/security/policy) on how to report security vulnerabilities. 70 | 71 | ## License 72 | 73 | Serializable Closure is open-sourced software licensed under the [MIT license](LICENSE.md). 74 | -------------------------------------------------------------------------------- /src/SerializableClosure.php: -------------------------------------------------------------------------------- 1 | serializable = Serializers\Signed::$signer 28 | ? new Serializers\Signed($closure) 29 | : new Serializers\Native($closure); 30 | } 31 | 32 | /** 33 | * Resolve the closure with the given arguments. 34 | * 35 | * @return mixed 36 | */ 37 | public function __invoke() 38 | { 39 | return call_user_func_array($this->serializable, func_get_args()); 40 | } 41 | 42 | /** 43 | * Gets the closure. 44 | * 45 | * @return \Closure 46 | */ 47 | public function getClosure() 48 | { 49 | return $this->serializable->getClosure(); 50 | } 51 | 52 | /** 53 | * Create a new unsigned serializable closure instance. 54 | * 55 | * @param Closure $closure 56 | * @return \Laravel\SerializableClosure\UnsignedSerializableClosure 57 | */ 58 | public static function unsigned(Closure $closure) 59 | { 60 | return new UnsignedSerializableClosure($closure); 61 | } 62 | 63 | /** 64 | * Sets the serializable closure secret key. 65 | * 66 | * @param string|null $secret 67 | * @return void 68 | */ 69 | public static function setSecretKey($secret) 70 | { 71 | Serializers\Signed::$signer = $secret 72 | ? new Hmac($secret) 73 | : null; 74 | } 75 | 76 | /** 77 | * Sets the serializable closure secret key. 78 | * 79 | * @param \Closure|null $transformer 80 | * @return void 81 | */ 82 | public static function transformUseVariablesUsing($transformer) 83 | { 84 | Serializers\Native::$transformUseVariables = $transformer; 85 | } 86 | 87 | /** 88 | * Sets the serializable closure secret key. 89 | * 90 | * @param \Closure|null $resolver 91 | * @return void 92 | */ 93 | public static function resolveUseVariablesUsing($resolver) 94 | { 95 | Serializers\Native::$resolveUseVariables = $resolver; 96 | } 97 | 98 | /** 99 | * Get the serializable representation of the closure. 100 | * 101 | * @return array{serializable: \Laravel\SerializableClosure\Serializers\Signed|\Laravel\SerializableClosure\Contracts\Serializable} 102 | */ 103 | public function __serialize() 104 | { 105 | return [ 106 | 'serializable' => $this->serializable, 107 | ]; 108 | } 109 | 110 | /** 111 | * Restore the closure after serialization. 112 | * 113 | * @param array{serializable: \Laravel\SerializableClosure\Serializers\Signed|\Laravel\SerializableClosure\Contracts\Serializable} $data 114 | * @return void 115 | * 116 | * @throws \Laravel\SerializableClosure\Exceptions\InvalidSignatureException 117 | */ 118 | public function __unserialize($data) 119 | { 120 | if (Signed::$signer && ! $data['serializable'] instanceof Signed) { 121 | throw new InvalidSignatureException(); 122 | } 123 | 124 | $this->serializable = $data['serializable']; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Support/ClosureStream.php: -------------------------------------------------------------------------------- 1 | content = "length = strlen($this->content); 56 | 57 | return true; 58 | } 59 | 60 | /** 61 | * Read from stream. 62 | * 63 | * @param int $count 64 | * @return string 65 | */ 66 | public function stream_read($count) 67 | { 68 | $value = substr($this->content, $this->pointer, $count); 69 | 70 | $this->pointer += $count; 71 | 72 | return $value; 73 | } 74 | 75 | /** 76 | * Tests for end-of-file on a file pointer. 77 | * 78 | * @return bool 79 | */ 80 | public function stream_eof() 81 | { 82 | return $this->pointer >= $this->length; 83 | } 84 | 85 | /** 86 | * Change stream options. 87 | * 88 | * @param int $option 89 | * @param int $arg1 90 | * @param int $arg2 91 | * @return bool 92 | */ 93 | public function stream_set_option($option, $arg1, $arg2) 94 | { 95 | return false; 96 | } 97 | 98 | /** 99 | * Retrieve information about a file resource. 100 | * 101 | * @return array|bool 102 | */ 103 | public function stream_stat() 104 | { 105 | $stat = stat(__FILE__); 106 | // @phpstan-ignore-next-line 107 | $stat[7] = $stat['size'] = $this->length; 108 | 109 | return $stat; 110 | } 111 | 112 | /** 113 | * Retrieve information about a file. 114 | * 115 | * @param string $path 116 | * @param int $flags 117 | * @return array|bool 118 | */ 119 | public function url_stat($path, $flags) 120 | { 121 | $stat = stat(__FILE__); 122 | // @phpstan-ignore-next-line 123 | $stat[7] = $stat['size'] = $this->length; 124 | 125 | return $stat; 126 | } 127 | 128 | /** 129 | * Seeks to specific location in a stream. 130 | * 131 | * @param int $offset 132 | * @param int $whence 133 | * @return bool 134 | */ 135 | public function stream_seek($offset, $whence = SEEK_SET) 136 | { 137 | $crt = $this->pointer; 138 | 139 | switch ($whence) { 140 | case SEEK_SET: 141 | $this->pointer = $offset; 142 | break; 143 | case SEEK_CUR: 144 | $this->pointer += $offset; 145 | break; 146 | case SEEK_END: 147 | $this->pointer = $this->length + $offset; 148 | break; 149 | } 150 | 151 | if ($this->pointer < 0 || $this->pointer >= $this->length) { 152 | $this->pointer = $crt; 153 | 154 | return false; 155 | } 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Retrieve the current position of a stream. 162 | * 163 | * @return int 164 | */ 165 | public function stream_tell() 166 | { 167 | return $this->pointer; 168 | } 169 | 170 | /** 171 | * Registers the stream. 172 | * 173 | * @return void 174 | */ 175 | public static function register() 176 | { 177 | if (! static::$isRegistered) { 178 | static::$isRegistered = stream_wrapper_register(static::STREAM_PROTO, __CLASS__); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Serializers/Native.php: -------------------------------------------------------------------------------- 1 | closure = $closure; 83 | } 84 | 85 | /** 86 | * Resolve the closure with the given arguments. 87 | * 88 | * @return mixed 89 | */ 90 | public function __invoke() 91 | { 92 | return call_user_func_array($this->closure, func_get_args()); 93 | } 94 | 95 | /** 96 | * Gets the closure. 97 | * 98 | * @return \Closure 99 | */ 100 | public function getClosure() 101 | { 102 | return $this->closure; 103 | } 104 | 105 | /** 106 | * Get the serializable representation of the closure. 107 | * 108 | * @return array 109 | */ 110 | public function __serialize() 111 | { 112 | if ($this->scope === null) { 113 | $this->scope = new ClosureScope(); 114 | $this->scope->toSerialize++; 115 | } 116 | 117 | $this->scope->serializations++; 118 | 119 | $scope = $object = null; 120 | $reflector = $this->getReflector(); 121 | 122 | if ($reflector->isBindingRequired()) { 123 | $object = $reflector->getClosureThis(); 124 | 125 | static::wrapClosures($object, $this->scope); 126 | } 127 | 128 | if ($scope = $reflector->getClosureScopeClass()) { 129 | $scope = $scope->name; 130 | } 131 | 132 | $this->reference = spl_object_hash($this->closure); 133 | 134 | $this->scope[$this->closure] = $this; 135 | 136 | $use = $reflector->getUseVariables(); 137 | 138 | if (static::$transformUseVariables) { 139 | $use = call_user_func(static::$transformUseVariables, $reflector->getUseVariables()); 140 | } 141 | 142 | $code = $reflector->getCode(); 143 | 144 | $this->mapByReference($use); 145 | 146 | $data = [ 147 | 'use' => $use, 148 | 'function' => $code, 149 | 'scope' => $scope, 150 | 'this' => $object, 151 | 'self' => $this->reference, 152 | ]; 153 | 154 | if (! --$this->scope->serializations && ! --$this->scope->toSerialize) { 155 | $this->scope = null; 156 | } 157 | 158 | return $data; 159 | } 160 | 161 | /** 162 | * Restore the closure after serialization. 163 | * 164 | * @param array $data 165 | * @return void 166 | */ 167 | public function __unserialize($data) 168 | { 169 | ClosureStream::register(); 170 | 171 | $this->code = $data; 172 | unset($data); 173 | 174 | $this->code['objects'] = []; 175 | 176 | if ($this->code['use']) { 177 | $this->scope = new ClosureScope(); 178 | 179 | if (static::$resolveUseVariables) { 180 | $this->code['use'] = call_user_func(static::$resolveUseVariables, $this->code['use']); 181 | } 182 | 183 | $this->mapPointers($this->code['use']); 184 | 185 | extract($this->code['use'], EXTR_OVERWRITE | EXTR_REFS); 186 | 187 | $this->scope = null; 188 | } 189 | 190 | $this->closure = include ClosureStream::STREAM_PROTO.'://'.$this->code['function']; 191 | 192 | if ($this->code['this'] === $this) { 193 | $this->code['this'] = null; 194 | } 195 | 196 | $this->closure = $this->closure->bindTo($this->code['this'], $this->code['scope']); 197 | 198 | if (! empty($this->code['objects'])) { 199 | foreach ($this->code['objects'] as $item) { 200 | $item['property']->setValue($item['instance'], $item['object']->getClosure()); 201 | } 202 | } 203 | 204 | $this->code = $this->code['function']; 205 | } 206 | 207 | /** 208 | * Ensures the given closures are serializable. 209 | * 210 | * @param mixed $data 211 | * @param \Laravel\SerializableClosure\Support\ClosureScope $storage 212 | * @return void 213 | */ 214 | public static function wrapClosures(&$data, $storage) 215 | { 216 | if ($data instanceof Closure) { 217 | $data = new static($data); 218 | } elseif (is_array($data)) { 219 | if (isset($data[self::ARRAY_RECURSIVE_KEY])) { 220 | return; 221 | } 222 | 223 | $data[self::ARRAY_RECURSIVE_KEY] = true; 224 | 225 | foreach ($data as $key => &$value) { 226 | if ($key === self::ARRAY_RECURSIVE_KEY) { 227 | continue; 228 | } 229 | static::wrapClosures($value, $storage); 230 | } 231 | 232 | unset($value); 233 | unset($data[self::ARRAY_RECURSIVE_KEY]); 234 | } elseif ($data instanceof \stdClass) { 235 | if (isset($storage[$data])) { 236 | $data = $storage[$data]; 237 | 238 | return; 239 | } 240 | 241 | $data = $storage[$data] = clone $data; 242 | 243 | foreach ($data as &$value) { 244 | static::wrapClosures($value, $storage); 245 | } 246 | 247 | unset($value); 248 | } elseif (is_object($data) && ! $data instanceof static && ! $data instanceof UnitEnum) { 249 | if (isset($storage[$data])) { 250 | $data = $storage[$data]; 251 | 252 | return; 253 | } 254 | 255 | $instance = $data; 256 | $reflection = new ReflectionObject($instance); 257 | 258 | if (! $reflection->isUserDefined()) { 259 | $storage[$instance] = $data; 260 | 261 | return; 262 | } 263 | 264 | $storage[$instance] = $data = $reflection->newInstanceWithoutConstructor(); 265 | 266 | do { 267 | if (! $reflection->isUserDefined()) { 268 | break; 269 | } 270 | 271 | foreach ($reflection->getProperties() as $property) { 272 | if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined()) { 273 | continue; 274 | } 275 | 276 | if (! $property->isInitialized($instance)) { 277 | continue; 278 | } 279 | 280 | $value = $property->getValue($instance); 281 | 282 | if (is_array($value) || is_object($value)) { 283 | static::wrapClosures($value, $storage); 284 | } 285 | 286 | $property->setValue($data, $value); 287 | } 288 | } while ($reflection = $reflection->getParentClass()); 289 | } 290 | } 291 | 292 | /** 293 | * Gets the closure's reflector. 294 | * 295 | * @return \Laravel\SerializableClosure\Support\ReflectionClosure 296 | */ 297 | public function getReflector() 298 | { 299 | if ($this->reflector === null) { 300 | $this->code = null; 301 | $this->reflector = new ReflectionClosure($this->closure); 302 | } 303 | 304 | return $this->reflector; 305 | } 306 | 307 | /** 308 | * Internal method used to map closure pointers. 309 | * 310 | * @param mixed $data 311 | * @return void 312 | */ 313 | protected function mapPointers(&$data) 314 | { 315 | $scope = $this->scope; 316 | 317 | if ($data instanceof static) { 318 | $data = &$data->closure; 319 | } elseif (is_array($data)) { 320 | if (isset($data[self::ARRAY_RECURSIVE_KEY])) { 321 | return; 322 | } 323 | 324 | $data[self::ARRAY_RECURSIVE_KEY] = true; 325 | 326 | foreach ($data as $key => &$value) { 327 | if ($key === self::ARRAY_RECURSIVE_KEY) { 328 | continue; 329 | } elseif ($value instanceof static) { 330 | $data[$key] = &$value->closure; 331 | } elseif ($value instanceof SelfReference && $value->hash === $this->code['self']) { 332 | $data[$key] = &$this->closure; 333 | } else { 334 | $this->mapPointers($value); 335 | } 336 | } 337 | 338 | unset($value); 339 | unset($data[self::ARRAY_RECURSIVE_KEY]); 340 | } elseif ($data instanceof \stdClass) { 341 | if (isset($scope[$data])) { 342 | return; 343 | } 344 | 345 | $scope[$data] = true; 346 | 347 | foreach ($data as $key => &$value) { 348 | if ($value instanceof SelfReference && $value->hash === $this->code['self']) { 349 | $data->{$key} = &$this->closure; 350 | } elseif (is_array($value) || is_object($value)) { 351 | $this->mapPointers($value); 352 | } 353 | } 354 | 355 | unset($value); 356 | } elseif (is_object($data) && ! ($data instanceof Closure)) { 357 | if (isset($scope[$data])) { 358 | return; 359 | } 360 | 361 | $scope[$data] = true; 362 | $reflection = new ReflectionObject($data); 363 | 364 | do { 365 | if (! $reflection->isUserDefined()) { 366 | break; 367 | } 368 | 369 | foreach ($reflection->getProperties() as $property) { 370 | if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined()) { 371 | continue; 372 | } 373 | 374 | if (! $property->isInitialized($data) || $property->isReadOnly()) { 375 | continue; 376 | } 377 | 378 | $item = $property->getValue($data); 379 | 380 | if ($item instanceof SerializableClosure || $item instanceof UnsignedSerializableClosure || ($item instanceof SelfReference && $item->hash === $this->code['self'])) { 381 | $this->code['objects'][] = [ 382 | 'instance' => $data, 383 | 'property' => $property, 384 | 'object' => $item instanceof SelfReference ? $this : $item, 385 | ]; 386 | } elseif (is_array($item) || is_object($item)) { 387 | $this->mapPointers($item); 388 | $property->setValue($data, $item); 389 | } 390 | } 391 | } while ($reflection = $reflection->getParentClass()); 392 | } 393 | } 394 | 395 | /** 396 | * Internal method used to map closures by reference. 397 | * 398 | * @param mixed $data 399 | * @return void 400 | */ 401 | protected function mapByReference(&$data) 402 | { 403 | if ($data instanceof Closure) { 404 | if ($data === $this->closure) { 405 | $data = new SelfReference($this->reference); 406 | 407 | return; 408 | } 409 | 410 | if (isset($this->scope[$data])) { 411 | $data = $this->scope[$data]; 412 | 413 | return; 414 | } 415 | 416 | $instance = new static($data); 417 | 418 | $instance->scope = $this->scope; 419 | 420 | $data = $this->scope[$data] = $instance; 421 | } elseif (is_array($data)) { 422 | if (isset($data[self::ARRAY_RECURSIVE_KEY])) { 423 | return; 424 | } 425 | 426 | $data[self::ARRAY_RECURSIVE_KEY] = true; 427 | 428 | foreach ($data as $key => &$value) { 429 | if ($key === self::ARRAY_RECURSIVE_KEY) { 430 | continue; 431 | } 432 | 433 | $this->mapByReference($value); 434 | } 435 | 436 | unset($value); 437 | unset($data[self::ARRAY_RECURSIVE_KEY]); 438 | } elseif ($data instanceof \stdClass) { 439 | if (isset($this->scope[$data])) { 440 | $data = $this->scope[$data]; 441 | 442 | return; 443 | } 444 | 445 | $instance = $data; 446 | $this->scope[$instance] = $data = clone $data; 447 | 448 | foreach ($data as &$value) { 449 | $this->mapByReference($value); 450 | } 451 | 452 | unset($value); 453 | } elseif (is_object($data) && ! $data instanceof SerializableClosure && ! $data instanceof UnsignedSerializableClosure) { 454 | if (isset($this->scope[$data])) { 455 | $data = $this->scope[$data]; 456 | 457 | return; 458 | } 459 | 460 | $instance = $data; 461 | 462 | if ($data instanceof DateTimeInterface) { 463 | $this->scope[$instance] = $data; 464 | 465 | return; 466 | } 467 | 468 | if ($data instanceof UnitEnum) { 469 | $this->scope[$instance] = $data; 470 | 471 | return; 472 | } 473 | 474 | $reflection = new ReflectionObject($data); 475 | 476 | if (! $reflection->isUserDefined()) { 477 | $this->scope[$instance] = $data; 478 | 479 | return; 480 | } 481 | 482 | $this->scope[$instance] = $data = $reflection->newInstanceWithoutConstructor(); 483 | 484 | do { 485 | if (! $reflection->isUserDefined()) { 486 | break; 487 | } 488 | 489 | foreach ($reflection->getProperties() as $property) { 490 | if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined() || $this->isVirtualProperty($property)) { 491 | continue; 492 | } 493 | 494 | if (! $property->isInitialized($instance) || ($property->isReadOnly() && $property->class !== $reflection->name)) { 495 | continue; 496 | } 497 | 498 | $value = $property->getValue($instance); 499 | 500 | if (is_array($value) || is_object($value)) { 501 | $this->mapByReference($value); 502 | } 503 | 504 | $property->setValue($data, $value); 505 | } 506 | } while ($reflection = $reflection->getParentClass()); 507 | } 508 | } 509 | 510 | /** 511 | * Determine is virtual property. 512 | * 513 | * @param \ReflectionProperty $property 514 | * @return bool 515 | */ 516 | protected function isVirtualProperty(ReflectionProperty $property): bool 517 | { 518 | return method_exists($property, 'isVirtual') && $property->isVirtual(); 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /src/Support/ReflectionClosure.php: -------------------------------------------------------------------------------- 1 | isStaticClosure === null) { 50 | $this->isStaticClosure = strtolower(substr($this->getCode(), 0, 6)) === 'static'; 51 | } 52 | 53 | return $this->isStaticClosure; 54 | } 55 | 56 | /** 57 | * Checks if the closure is a "short closure". 58 | * 59 | * @return bool 60 | */ 61 | public function isShortClosure() 62 | { 63 | if ($this->isShortClosure === null) { 64 | $code = $this->getCode(); 65 | 66 | if ($this->isStatic()) { 67 | $code = substr($code, 6); 68 | } 69 | 70 | $this->isShortClosure = strtolower(substr(trim($code), 0, 2)) === 'fn'; 71 | } 72 | 73 | return $this->isShortClosure; 74 | } 75 | 76 | /** 77 | * Get the closure's code. 78 | * 79 | * @return string 80 | */ 81 | public function getCode() 82 | { 83 | if ($this->code !== null) { 84 | return $this->code; 85 | } 86 | 87 | $fileName = $this->getFileName(); 88 | $line = $this->getStartLine() - 1; 89 | 90 | $className = null; 91 | 92 | if (null !== $className = $this->getClosureScopeClass()) { 93 | $className = '\\'.trim($className->getName(), '\\'); 94 | } 95 | 96 | $builtin_types = self::getBuiltinTypes(); 97 | $class_keywords = ['self', 'static', 'parent']; 98 | 99 | $ns = $this->getClosureNamespaceName(); 100 | $nsf = $ns == '' ? '' : ($ns[0] == '\\' ? $ns : '\\'.$ns); 101 | 102 | $_file = var_export($fileName, true); 103 | $_dir = var_export(dirname($fileName), true); 104 | $_namespace = var_export($ns, true); 105 | $_class = var_export(trim($className ?: '', '\\'), true); 106 | $_function = $ns.($ns == '' ? '' : '\\').'{closure}'; 107 | $_method = ($className == '' ? '' : trim($className, '\\').'::').$_function; 108 | $_function = var_export($_function, true); 109 | $_method = var_export($_method, true); 110 | $_trait = null; 111 | 112 | $tokens = $this->getTokens(); 113 | $state = $lastState = 'start'; 114 | $inside_structure = false; 115 | $isFirstClassCallable = false; 116 | $isShortClosure = false; 117 | 118 | $inside_structure_mark = 0; 119 | $open = 0; 120 | $code = ''; 121 | $id_start = $id_start_ci = $id_name = $context = ''; 122 | $classes = $functions = $constants = null; 123 | $use = []; 124 | $lineAdd = 0; 125 | $isUsingScope = false; 126 | $isUsingThisObject = false; 127 | 128 | for ($i = 0, $l = count($tokens); $i < $l; $i++) { 129 | $token = $tokens[$i]; 130 | 131 | switch ($state) { 132 | case 'start': 133 | if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) { 134 | $code .= $token[1]; 135 | 136 | $state = $token[0] === T_FUNCTION ? 'function' : 'static'; 137 | } elseif ($token[0] === T_FN) { 138 | $isShortClosure = true; 139 | $code .= $token[1]; 140 | $state = 'closure_args'; 141 | } elseif ($token[0] === T_PUBLIC || $token[0] === T_PROTECTED || $token[0] === T_PRIVATE) { 142 | $code = ''; 143 | $isFirstClassCallable = true; 144 | } 145 | break; 146 | case 'static': 147 | if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_FUNCTION) { 148 | $code .= $token[1]; 149 | if ($token[0] === T_FUNCTION) { 150 | $state = 'function'; 151 | } 152 | } elseif ($token[0] === T_FN) { 153 | $isShortClosure = true; 154 | $code .= $token[1]; 155 | $state = 'closure_args'; 156 | } else { 157 | $code = ''; 158 | $state = 'start'; 159 | } 160 | break; 161 | case 'function': 162 | switch ($token[0]) { 163 | case T_STRING: 164 | if ($isFirstClassCallable) { 165 | $state = 'closure_args'; 166 | break; 167 | } 168 | 169 | $code = ''; 170 | $state = 'named_function'; 171 | break; 172 | case '(': 173 | $code .= '('; 174 | $state = 'closure_args'; 175 | break; 176 | default: 177 | $code .= is_array($token) ? $token[1] : $token; 178 | } 179 | break; 180 | case 'named_function': 181 | if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) { 182 | $code = $token[1]; 183 | $state = $token[0] === T_FUNCTION ? 'function' : 'static'; 184 | } elseif ($token[0] === T_FN) { 185 | $isShortClosure = true; 186 | $code .= $token[1]; 187 | $state = 'closure_args'; 188 | } 189 | break; 190 | case 'closure_args': 191 | switch ($token[0]) { 192 | case T_NAME_QUALIFIED: 193 | [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); 194 | $context = 'args'; 195 | $state = 'id_name'; 196 | $lastState = 'closure_args'; 197 | break; 198 | case T_NS_SEPARATOR: 199 | case T_STRING: 200 | $id_start = $token[1]; 201 | $id_start_ci = strtolower($id_start); 202 | $id_name = ''; 203 | $context = 'args'; 204 | $state = 'id_name'; 205 | $lastState = 'closure_args'; 206 | break; 207 | case T_USE: 208 | $code .= $token[1]; 209 | $state = 'use'; 210 | break; 211 | case T_DOUBLE_ARROW: 212 | $code .= $token[1]; 213 | if ($isShortClosure) { 214 | $state = 'closure'; 215 | } 216 | break; 217 | case ':': 218 | $code .= ':'; 219 | $state = 'return'; 220 | break; 221 | case '{': 222 | $code .= '{'; 223 | $state = 'closure'; 224 | $open++; 225 | break; 226 | default: 227 | $code .= is_array($token) ? $token[1] : $token; 228 | } 229 | break; 230 | case 'use': 231 | switch ($token[0]) { 232 | case T_VARIABLE: 233 | $use[] = substr($token[1], 1); 234 | $code .= $token[1]; 235 | break; 236 | case '{': 237 | $code .= '{'; 238 | $state = 'closure'; 239 | $open++; 240 | break; 241 | case ':': 242 | $code .= ':'; 243 | $state = 'return'; 244 | break; 245 | default: 246 | $code .= is_array($token) ? $token[1] : $token; 247 | break; 248 | } 249 | break; 250 | case 'return': 251 | switch ($token[0]) { 252 | case T_WHITESPACE: 253 | case T_COMMENT: 254 | case T_DOC_COMMENT: 255 | $code .= $token[1]; 256 | break; 257 | case T_NS_SEPARATOR: 258 | case T_STRING: 259 | $id_start = $token[1]; 260 | $id_start_ci = strtolower($id_start); 261 | $id_name = ''; 262 | $context = 'return_type'; 263 | $state = 'id_name'; 264 | $lastState = 'return'; 265 | break 2; 266 | case T_NAME_QUALIFIED: 267 | [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); 268 | $context = 'return_type'; 269 | $state = 'id_name'; 270 | $lastState = 'return'; 271 | break 2; 272 | case T_DOUBLE_ARROW: 273 | $code .= $token[1]; 274 | if ($isShortClosure) { 275 | $state = 'closure'; 276 | } 277 | break; 278 | case '{': 279 | $code .= '{'; 280 | $state = 'closure'; 281 | $open++; 282 | break; 283 | default: 284 | $code .= is_array($token) ? $token[1] : $token; 285 | break; 286 | } 287 | break; 288 | case 'closure': 289 | switch ($token[0]) { 290 | case T_CURLY_OPEN: 291 | case T_DOLLAR_OPEN_CURLY_BRACES: 292 | case '{': 293 | $code .= is_array($token) ? $token[1] : $token; 294 | $open++; 295 | break; 296 | case '}': 297 | $code .= '}'; 298 | if (--$open === 0 && ! $isShortClosure) { 299 | break 3; 300 | } elseif ($inside_structure) { 301 | $inside_structure = ! ($open === $inside_structure_mark); 302 | } 303 | break; 304 | case '(': 305 | case '[': 306 | $code .= $token[0]; 307 | if ($isShortClosure) { 308 | $open++; 309 | } 310 | break; 311 | case ')': 312 | case ']': 313 | if ($isShortClosure) { 314 | if ($open === 0) { 315 | break 3; 316 | } 317 | $open--; 318 | } 319 | $code .= $token[0]; 320 | break; 321 | case ',': 322 | case ';': 323 | if ($isShortClosure && $open === 0) { 324 | break 3; 325 | } 326 | $code .= $token[0]; 327 | break; 328 | case T_LINE: 329 | $code .= $token[2] - $line + $lineAdd; 330 | break; 331 | case T_FILE: 332 | $code .= $_file; 333 | break; 334 | case T_DIR: 335 | $code .= $_dir; 336 | break; 337 | case T_NS_C: 338 | $code .= $_namespace; 339 | break; 340 | case T_CLASS_C: 341 | $code .= $inside_structure ? $token[1] : $_class; 342 | break; 343 | case T_FUNC_C: 344 | $code .= $inside_structure ? $token[1] : $_function; 345 | break; 346 | case T_METHOD_C: 347 | $code .= $inside_structure ? $token[1] : $_method; 348 | break; 349 | case T_COMMENT: 350 | if (substr($token[1], 0, 8) === '#trackme') { 351 | $timestamp = time(); 352 | $code .= '/**'.PHP_EOL; 353 | $code .= '* Date : '.date(DATE_W3C, $timestamp).PHP_EOL; 354 | $code .= '* Timestamp : '.$timestamp.PHP_EOL; 355 | $code .= '* Line : '.($line + 1).PHP_EOL; 356 | $code .= '* File : '.$_file.PHP_EOL.'*/'.PHP_EOL; 357 | $lineAdd += 5; 358 | } else { 359 | $code .= $token[1]; 360 | } 361 | break; 362 | case T_VARIABLE: 363 | if ($token[1] == '$this' && ! $inside_structure) { 364 | $isUsingThisObject = true; 365 | } 366 | $code .= $token[1]; 367 | break; 368 | case T_STATIC: 369 | case T_NS_SEPARATOR: 370 | case T_STRING: 371 | $id_start = $token[1]; 372 | $id_start_ci = strtolower($id_start); 373 | $id_name = ''; 374 | $context = 'root'; 375 | $state = 'id_name'; 376 | $lastState = 'closure'; 377 | break 2; 378 | case T_NAME_QUALIFIED: 379 | [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); 380 | $context = 'root'; 381 | $state = 'id_name'; 382 | $lastState = 'closure'; 383 | break 2; 384 | case T_NEW: 385 | $code .= $token[1]; 386 | $context = 'new'; 387 | $state = 'id_start'; 388 | $lastState = 'closure'; 389 | break 2; 390 | case T_USE: 391 | $code .= $token[1]; 392 | $context = 'use'; 393 | $state = 'id_start'; 394 | $lastState = 'closure'; 395 | break; 396 | case T_INSTANCEOF: 397 | case T_INSTEADOF: 398 | $code .= $token[1]; 399 | $context = 'instanceof'; 400 | $state = 'id_start'; 401 | $lastState = 'closure'; 402 | break; 403 | case T_OBJECT_OPERATOR: 404 | case T_NULLSAFE_OBJECT_OPERATOR: 405 | case T_DOUBLE_COLON: 406 | $code .= $token[1]; 407 | $lastState = 'closure'; 408 | $state = 'ignore_next'; 409 | break; 410 | case T_FUNCTION: 411 | $code .= $token[1]; 412 | $state = 'closure_args'; 413 | if (! $inside_structure) { 414 | $inside_structure = true; 415 | $inside_structure_mark = $open; 416 | } 417 | break; 418 | case T_TRAIT_C: 419 | if ($_trait === null) { 420 | $startLine = $this->getStartLine(); 421 | $endLine = $this->getEndLine(); 422 | $structures = $this->getStructures(); 423 | 424 | $_trait = ''; 425 | 426 | foreach ($structures as &$struct) { 427 | if ($struct['type'] === 'trait' && 428 | $struct['start'] <= $startLine && 429 | $struct['end'] >= $endLine 430 | ) { 431 | $_trait = ($ns == '' ? '' : $ns.'\\').$struct['name']; 432 | break; 433 | } 434 | } 435 | 436 | $_trait = var_export($_trait, true); 437 | } 438 | 439 | $code .= $_trait; 440 | break; 441 | default: 442 | $code .= is_array($token) ? $token[1] : $token; 443 | } 444 | break; 445 | case 'ignore_next': 446 | switch ($token[0]) { 447 | case T_WHITESPACE: 448 | case T_COMMENT: 449 | case T_DOC_COMMENT: 450 | $code .= $token[1]; 451 | break; 452 | case T_CLASS: 453 | case T_NEW: 454 | case T_STATIC: 455 | case T_VARIABLE: 456 | case T_STRING: 457 | case T_CLASS_C: 458 | case T_FILE: 459 | case T_DIR: 460 | case T_METHOD_C: 461 | case T_FUNC_C: 462 | case T_FUNCTION: 463 | case T_INSTANCEOF: 464 | case T_LINE: 465 | case T_NS_C: 466 | case T_TRAIT_C: 467 | case T_USE: 468 | $code .= $token[1]; 469 | $state = $lastState; 470 | break; 471 | default: 472 | $state = $lastState; 473 | $i--; 474 | } 475 | break; 476 | case 'id_start': 477 | switch ($token[0]) { 478 | case T_WHITESPACE: 479 | case T_COMMENT: 480 | case T_DOC_COMMENT: 481 | $code .= $token[1]; 482 | break; 483 | case T_NS_SEPARATOR: 484 | case T_NAME_FULLY_QUALIFIED: 485 | case T_STRING: 486 | case T_STATIC: 487 | $id_start = $token[1]; 488 | $id_start_ci = strtolower($id_start); 489 | $id_name = ''; 490 | $state = 'id_name'; 491 | break 2; 492 | case T_NAME_QUALIFIED: 493 | [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); 494 | $state = 'id_name'; 495 | break 2; 496 | case T_VARIABLE: 497 | $code .= $token[1]; 498 | $state = $lastState; 499 | break; 500 | case T_CLASS: 501 | $code .= $token[1]; 502 | $state = 'anonymous'; 503 | break; 504 | default: 505 | $i--; //reprocess last 506 | $state = 'id_name'; 507 | } 508 | break; 509 | case 'id_name': 510 | switch ($token[0]) { 511 | case $token[0] === ':' && ! in_array($context, ['instanceof', 'new'], true): 512 | if ($lastState === 'closure' && $context === 'root') { 513 | $state = 'closure'; 514 | $code .= $id_start.$token; 515 | } 516 | 517 | break; 518 | case T_NAME_QUALIFIED: 519 | case T_NS_SEPARATOR: 520 | case T_STRING: 521 | case T_WHITESPACE: 522 | case T_COMMENT: 523 | case T_DOC_COMMENT: 524 | $id_name .= $token[1]; 525 | break; 526 | case '(': 527 | if ($isShortClosure) { 528 | $open++; 529 | } 530 | if ($context === 'new' || false !== strpos($id_name, '\\')) { 531 | if ($id_start_ci === 'self' || $id_start_ci === 'static') { 532 | if (! $inside_structure) { 533 | $isUsingScope = true; 534 | } 535 | } elseif ($id_start !== '\\' && ! in_array($id_start_ci, $class_keywords)) { 536 | if ($classes === null) { 537 | $classes = $this->getClasses(); 538 | } 539 | if (isset($classes[$id_start_ci])) { 540 | $id_start = $classes[$id_start_ci]; 541 | } 542 | if ($id_start[0] !== '\\') { 543 | $id_start = $nsf.'\\'.$id_start; 544 | } 545 | } 546 | } else { 547 | if ($id_start !== '\\') { 548 | if ($functions === null) { 549 | $functions = $this->getFunctions(); 550 | } 551 | if (isset($functions[$id_start_ci])) { 552 | $id_start = $functions[$id_start_ci]; 553 | } elseif ($nsf !== '\\' && function_exists($nsf.'\\'.$id_start)) { 554 | $id_start = $nsf.'\\'.$id_start; 555 | // Cache it to functions array 556 | $functions[$id_start_ci] = $id_start; 557 | } 558 | } 559 | } 560 | $code .= $id_start.$id_name.'('; 561 | $state = $lastState; 562 | break; 563 | case T_VARIABLE: 564 | case T_DOUBLE_COLON: 565 | if ($id_start !== '\\') { 566 | if ($id_start_ci === 'self' || $id_start_ci === 'parent') { 567 | if (! $inside_structure) { 568 | $isUsingScope = true; 569 | } 570 | } elseif ($id_start_ci === 'static') { 571 | if (! $inside_structure) { 572 | $isUsingScope = $token[0] === T_DOUBLE_COLON; 573 | } 574 | } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { 575 | if ($classes === null) { 576 | $classes = $this->getClasses(); 577 | } 578 | if (isset($classes[$id_start_ci])) { 579 | $id_start = $classes[$id_start_ci]; 580 | } 581 | if ($id_start[0] !== '\\') { 582 | $id_start = $nsf.'\\'.$id_start; 583 | } 584 | } 585 | } 586 | 587 | $code .= $id_start.$id_name.$token[1]; 588 | $state = $token[0] === T_DOUBLE_COLON ? 'ignore_next' : $lastState; 589 | break; 590 | default: 591 | if ($id_start !== '\\' && ! defined($id_start)) { 592 | if ($constants === null) { 593 | $constants = $this->getConstants(); 594 | } 595 | if (isset($constants[$id_start])) { 596 | $id_start = $constants[$id_start]; 597 | } elseif ($context === 'new') { 598 | if (in_array($id_start_ci, $class_keywords)) { 599 | if (! $inside_structure) { 600 | $isUsingScope = true; 601 | } 602 | } else { 603 | if ($classes === null) { 604 | $classes = $this->getClasses(); 605 | } 606 | if (isset($classes[$id_start_ci])) { 607 | $id_start = $classes[$id_start_ci]; 608 | } 609 | if ($id_start[0] !== '\\') { 610 | $id_start = $nsf.'\\'.$id_start; 611 | } 612 | } 613 | } elseif ($context === 'use' || 614 | $context === 'instanceof' || 615 | $context === 'args' || 616 | $context === 'return_type' || 617 | $context === 'extends' || 618 | $context === 'root' 619 | ) { 620 | if (in_array($id_start_ci, $class_keywords)) { 621 | if (! $inside_structure && ! $id_start_ci === 'static') { 622 | $isUsingScope = true; 623 | } 624 | } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { 625 | if ($classes === null) { 626 | $classes = $this->getClasses(); 627 | } 628 | if (isset($classes[$id_start_ci])) { 629 | $id_start = $classes[$id_start_ci]; 630 | } 631 | if ($id_start[0] !== '\\') { 632 | $id_start = $nsf.'\\'.$id_start; 633 | } 634 | } 635 | } 636 | } 637 | $code .= $id_start.$id_name; 638 | $state = $lastState; 639 | $i--; //reprocess last token 640 | } 641 | break; 642 | case 'anonymous': 643 | switch ($token[0]) { 644 | case T_NAME_QUALIFIED: 645 | [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); 646 | $state = 'id_name'; 647 | $lastState = 'anonymous'; 648 | break 2; 649 | case T_NS_SEPARATOR: 650 | case T_STRING: 651 | $id_start = $token[1]; 652 | $id_start_ci = strtolower($id_start); 653 | $id_name = ''; 654 | $state = 'id_name'; 655 | $context = 'extends'; 656 | $lastState = 'anonymous'; 657 | break; 658 | case '{': 659 | $state = 'closure'; 660 | if (! $inside_structure) { 661 | $inside_structure = true; 662 | $inside_structure_mark = $open; 663 | } 664 | $i--; 665 | break; 666 | default: 667 | $code .= is_array($token) ? $token[1] : $token; 668 | } 669 | break; 670 | } 671 | } 672 | 673 | if ($isShortClosure) { 674 | $this->useVariables = $this->getStaticVariables(); 675 | } else { 676 | $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); 677 | } 678 | 679 | $this->isShortClosure = $isShortClosure; 680 | $this->isBindingRequired = $isUsingThisObject; 681 | $this->isScopeRequired = $isUsingScope; 682 | 683 | $attributesCode = array_map(function ($attribute) { 684 | $arguments = $attribute->getArguments(); 685 | 686 | $name = $attribute->getName(); 687 | $arguments = implode(', ', array_map(function ($argument, $key) { 688 | $argument = var_export($argument, true); 689 | 690 | if (is_string($key)) { 691 | $argument = sprintf('%s: %s', $key, $argument); 692 | } 693 | 694 | return $argument; 695 | }, $arguments, array_keys($arguments))); 696 | 697 | return "#[$name($arguments)]"; 698 | }, $this->getAttributes()); 699 | 700 | if (! empty($attributesCode)) { 701 | $code = implode("\n", array_merge($attributesCode, [$code])); 702 | } 703 | 704 | $this->code = $code; 705 | 706 | return $this->code; 707 | } 708 | 709 | /** 710 | * Get PHP native built in types. 711 | * 712 | * @return array 713 | */ 714 | protected static function getBuiltinTypes() 715 | { 716 | return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null', 'never']; 717 | } 718 | 719 | /** 720 | * Gets the use variables by the closure. 721 | * 722 | * @return array 723 | */ 724 | public function getUseVariables() 725 | { 726 | if ($this->useVariables !== null) { 727 | return $this->useVariables; 728 | } 729 | 730 | $tokens = $this->getTokens(); 731 | $use = []; 732 | $state = 'start'; 733 | 734 | foreach ($tokens as &$token) { 735 | $is_array = is_array($token); 736 | 737 | switch ($state) { 738 | case 'start': 739 | if ($is_array && $token[0] === T_USE) { 740 | $state = 'use'; 741 | } 742 | break; 743 | case 'use': 744 | if ($is_array) { 745 | if ($token[0] === T_VARIABLE) { 746 | $use[] = substr($token[1], 1); 747 | } 748 | } elseif ($token == ')') { 749 | break 2; 750 | } 751 | break; 752 | } 753 | } 754 | 755 | $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); 756 | 757 | return $this->useVariables; 758 | } 759 | 760 | /** 761 | * Checks if binding is required. 762 | * 763 | * @return bool 764 | */ 765 | public function isBindingRequired() 766 | { 767 | if ($this->isBindingRequired === null) { 768 | $this->getCode(); 769 | } 770 | 771 | return $this->isBindingRequired; 772 | } 773 | 774 | /** 775 | * Checks if access to the scope is required. 776 | * 777 | * @return bool 778 | */ 779 | public function isScopeRequired() 780 | { 781 | if ($this->isScopeRequired === null) { 782 | $this->getCode(); 783 | } 784 | 785 | return $this->isScopeRequired; 786 | } 787 | 788 | /** 789 | * The hash of the current file name. 790 | * 791 | * @return string 792 | */ 793 | protected function getHashedFileName() 794 | { 795 | if ($this->hashedName === null) { 796 | $this->hashedName = sha1($this->getFileName()); 797 | } 798 | 799 | return $this->hashedName; 800 | } 801 | 802 | /** 803 | * Get the file tokens. 804 | * 805 | * @return array 806 | */ 807 | protected function getFileTokens() 808 | { 809 | $key = $this->getHashedFileName(); 810 | 811 | if (! isset(static::$files[$key])) { 812 | static::$files[$key] = token_get_all(file_get_contents($this->getFileName())); 813 | } 814 | 815 | return static::$files[$key]; 816 | } 817 | 818 | /** 819 | * Get the tokens. 820 | * 821 | * @return array 822 | */ 823 | protected function getTokens() 824 | { 825 | if ($this->tokens === null) { 826 | $tokens = $this->getFileTokens(); 827 | $startLine = $this->getStartLine(); 828 | $endLine = $this->getEndLine(); 829 | $results = []; 830 | $start = false; 831 | 832 | foreach ($tokens as &$token) { 833 | if (! is_array($token)) { 834 | if ($start) { 835 | $results[] = $token; 836 | } 837 | 838 | continue; 839 | } 840 | 841 | $line = $token[2]; 842 | 843 | if ($line <= $endLine) { 844 | if ($line >= $startLine) { 845 | $start = true; 846 | $results[] = $token; 847 | } 848 | 849 | continue; 850 | } 851 | 852 | break; 853 | } 854 | 855 | $this->tokens = $results; 856 | } 857 | 858 | return $this->tokens; 859 | } 860 | 861 | /** 862 | * Get the classes. 863 | * 864 | * @return array 865 | */ 866 | protected function getClasses() 867 | { 868 | $line = $this->getStartLine(); 869 | 870 | foreach ($this->getStructures() as $struct) { 871 | if ($struct['type'] === 'namespace' && 872 | $struct['start'] <= $line && 873 | $struct['end'] >= $line 874 | ) { 875 | return $struct['classes']; 876 | } 877 | } 878 | 879 | return []; 880 | } 881 | 882 | /** 883 | * Get the functions. 884 | * 885 | * @return array 886 | */ 887 | protected function getFunctions() 888 | { 889 | $key = $this->getHashedFileName(); 890 | 891 | if (! isset(static::$functions[$key])) { 892 | $this->fetchItems(); 893 | } 894 | 895 | return static::$functions[$key]; 896 | } 897 | 898 | /** 899 | * Gets the constants. 900 | * 901 | * @return array 902 | */ 903 | protected function getConstants() 904 | { 905 | $key = $this->getHashedFileName(); 906 | 907 | if (! isset(static::$constants[$key])) { 908 | $this->fetchItems(); 909 | } 910 | 911 | return static::$constants[$key]; 912 | } 913 | 914 | /** 915 | * Get the structures. 916 | * 917 | * @return array 918 | */ 919 | protected function getStructures() 920 | { 921 | $key = $this->getHashedFileName(); 922 | 923 | if (! isset(static::$structures[$key])) { 924 | $this->fetchItems(); 925 | } 926 | 927 | return static::$structures[$key]; 928 | } 929 | 930 | /** 931 | * Fetch the items. 932 | * 933 | * @return void. 934 | */ 935 | protected function fetchItems() 936 | { 937 | $key = $this->getHashedFileName(); 938 | 939 | $classes = []; 940 | $functions = []; 941 | $constants = []; 942 | $structures = []; 943 | $tokens = $this->getFileTokens(); 944 | 945 | $open = 0; 946 | $state = 'start'; 947 | $lastState = ''; 948 | $prefix = ''; 949 | $name = ''; 950 | $alias = ''; 951 | $isFunc = $isConst = false; 952 | 953 | $startLine = $lastKnownLine = 0; 954 | $structType = $structName = ''; 955 | $structIgnore = false; 956 | 957 | $namespace = ''; 958 | $namespaceStartLine = 0; 959 | $namespaceBraced = false; 960 | $namespaceClasses = []; 961 | 962 | foreach ($tokens as $token) { 963 | if (is_array($token)) { 964 | $lastKnownLine = $token[2]; 965 | } 966 | 967 | switch ($state) { 968 | case 'start': 969 | switch ($token[0]) { 970 | case T_NAMESPACE: 971 | $structures[] = [ 972 | 'type' => 'namespace', 973 | 'name' => $namespace, 974 | 'start' => $namespaceStartLine, 975 | 'end' => $token[2] - 1, 976 | 'classes' => $namespaceClasses, 977 | ]; 978 | $namespace = ''; 979 | $namespaceClasses = []; 980 | $state = 'namespace'; 981 | $namespaceStartLine = $token[2]; 982 | break; 983 | case T_CLASS: 984 | case T_INTERFACE: 985 | case T_TRAIT: 986 | $state = 'before_structure'; 987 | $startLine = $token[2]; 988 | $structType = $token[0] == T_CLASS 989 | ? 'class' 990 | : ($token[0] == T_INTERFACE ? 'interface' : 'trait'); 991 | break; 992 | case T_USE: 993 | $state = 'use'; 994 | $prefix = $name = $alias = ''; 995 | $isFunc = $isConst = false; 996 | break; 997 | case T_FUNCTION: 998 | $state = 'structure'; 999 | $structIgnore = true; 1000 | break; 1001 | case T_NEW: 1002 | $state = 'new'; 1003 | break; 1004 | case T_OBJECT_OPERATOR: 1005 | case T_DOUBLE_COLON: 1006 | $state = 'invoke'; 1007 | break; 1008 | case '}': 1009 | if ($namespaceBraced) { 1010 | $structures[] = [ 1011 | 'type' => 'namespace', 1012 | 'name' => $namespace, 1013 | 'start' => $namespaceStartLine, 1014 | 'end' => $lastKnownLine, 1015 | 'classes' => $namespaceClasses, 1016 | ]; 1017 | $namespaceBraced = false; 1018 | $namespace = ''; 1019 | $namespaceClasses = []; 1020 | } 1021 | break; 1022 | } 1023 | break; 1024 | case 'namespace': 1025 | switch ($token[0]) { 1026 | case T_STRING: 1027 | case T_NAME_QUALIFIED: 1028 | $namespace = $token[1]; 1029 | break; 1030 | case ';': 1031 | case '{': 1032 | $state = 'start'; 1033 | $namespaceBraced = $token[0] === '{'; 1034 | break; 1035 | } 1036 | break; 1037 | case 'use': 1038 | switch ($token[0]) { 1039 | case T_FUNCTION: 1040 | $isFunc = true; 1041 | break; 1042 | case T_CONST: 1043 | $isConst = true; 1044 | break; 1045 | case T_NS_SEPARATOR: 1046 | $name .= $token[1]; 1047 | break; 1048 | case T_STRING: 1049 | $name .= $token[1]; 1050 | $alias = $token[1]; 1051 | break; 1052 | case T_NAME_QUALIFIED: 1053 | $name .= $token[1]; 1054 | $pieces = explode('\\', $token[1]); 1055 | $alias = end($pieces); 1056 | break; 1057 | case T_AS: 1058 | $lastState = 'use'; 1059 | $state = 'alias'; 1060 | break; 1061 | case '{': 1062 | $prefix = $name; 1063 | $name = $alias = ''; 1064 | $state = 'use-group'; 1065 | break; 1066 | case ',': 1067 | case ';': 1068 | if ($name === '' || $name[0] !== '\\') { 1069 | $name = '\\'.$name; 1070 | } 1071 | 1072 | if ($alias !== '') { 1073 | if ($isFunc) { 1074 | $functions[strtolower($alias)] = $name; 1075 | } elseif ($isConst) { 1076 | $constants[$alias] = $name; 1077 | } else { 1078 | $classes[strtolower($alias)] = $name; 1079 | $namespaceClasses[strtolower($alias)] = $name; 1080 | } 1081 | } 1082 | $name = $alias = ''; 1083 | $state = $token === ';' ? 'start' : 'use'; 1084 | break; 1085 | } 1086 | break; 1087 | case 'use-group': 1088 | switch ($token[0]) { 1089 | case T_NS_SEPARATOR: 1090 | $name .= $token[1]; 1091 | break; 1092 | case T_NAME_QUALIFIED: 1093 | $name .= $token[1]; 1094 | $pieces = explode('\\', $token[1]); 1095 | $alias = end($pieces); 1096 | break; 1097 | case T_STRING: 1098 | $name .= $token[1]; 1099 | $alias = $token[1]; 1100 | break; 1101 | case T_AS: 1102 | $lastState = 'use-group'; 1103 | $state = 'alias'; 1104 | break; 1105 | case ',': 1106 | case '}': 1107 | 1108 | if ($prefix === '' || $prefix[0] !== '\\') { 1109 | $prefix = '\\'.$prefix; 1110 | } 1111 | 1112 | if ($alias !== '') { 1113 | if ($isFunc) { 1114 | $functions[strtolower($alias)] = $prefix.$name; 1115 | } elseif ($isConst) { 1116 | $constants[$alias] = $prefix.$name; 1117 | } else { 1118 | $classes[strtolower($alias)] = $prefix.$name; 1119 | $namespaceClasses[strtolower($alias)] = $prefix.$name; 1120 | } 1121 | } 1122 | $name = $alias = ''; 1123 | $state = $token === '}' ? 'use' : 'use-group'; 1124 | break; 1125 | } 1126 | break; 1127 | case 'alias': 1128 | if ($token[0] === T_STRING) { 1129 | $alias = $token[1]; 1130 | $state = $lastState; 1131 | } 1132 | break; 1133 | case 'new': 1134 | switch ($token[0]) { 1135 | case T_WHITESPACE: 1136 | case T_COMMENT: 1137 | case T_DOC_COMMENT: 1138 | break 2; 1139 | case T_CLASS: 1140 | $state = 'structure'; 1141 | $structIgnore = true; 1142 | break; 1143 | default: 1144 | $state = 'start'; 1145 | } 1146 | break; 1147 | case 'invoke': 1148 | switch ($token[0]) { 1149 | case T_WHITESPACE: 1150 | case T_COMMENT: 1151 | case T_DOC_COMMENT: 1152 | break 2; 1153 | default: 1154 | $state = 'start'; 1155 | } 1156 | break; 1157 | case 'before_structure': 1158 | if ($token[0] == T_STRING) { 1159 | $structName = $token[1]; 1160 | $state = 'structure'; 1161 | } 1162 | break; 1163 | case 'structure': 1164 | switch ($token[0]) { 1165 | case '{': 1166 | case T_CURLY_OPEN: 1167 | case T_DOLLAR_OPEN_CURLY_BRACES: 1168 | $open++; 1169 | break; 1170 | case '}': 1171 | if (--$open == 0) { 1172 | if (! $structIgnore) { 1173 | $structures[] = [ 1174 | 'type' => $structType, 1175 | 'name' => $structName, 1176 | 'start' => $startLine, 1177 | 'end' => $lastKnownLine, 1178 | ]; 1179 | } 1180 | $structIgnore = false; 1181 | $state = 'start'; 1182 | } 1183 | break; 1184 | } 1185 | break; 1186 | } 1187 | } 1188 | 1189 | $structures[] = [ 1190 | 'type' => 'namespace', 1191 | 'name' => $namespace, 1192 | 'start' => $namespaceStartLine, 1193 | 'end' => PHP_INT_MAX, 1194 | 'classes' => $namespaceClasses, 1195 | ]; 1196 | 1197 | static::$classes[$key] = $classes; 1198 | static::$functions[$key] = $functions; 1199 | static::$constants[$key] = $constants; 1200 | static::$structures[$key] = $structures; 1201 | } 1202 | 1203 | /** 1204 | * Returns the namespace associated to the closure. 1205 | * 1206 | * @return string 1207 | */ 1208 | protected function getClosureNamespaceName() 1209 | { 1210 | $startLine = $this->getStartLine(); 1211 | $endLine = $this->getEndLine(); 1212 | 1213 | foreach ($this->getStructures() as $struct) { 1214 | if ($struct['type'] === 'namespace' && 1215 | $struct['start'] <= $startLine && 1216 | $struct['end'] >= $endLine 1217 | ) { 1218 | return $struct['name']; 1219 | } 1220 | } 1221 | 1222 | return ''; 1223 | } 1224 | 1225 | /** 1226 | * Parse the given token. 1227 | * 1228 | * @param string $token 1229 | * @return array 1230 | */ 1231 | protected function parseNameQualified($token) 1232 | { 1233 | $pieces = explode('\\', $token); 1234 | 1235 | $id_start = array_shift($pieces); 1236 | 1237 | $id_start_ci = strtolower($id_start); 1238 | 1239 | $id_name = '\\'.implode('\\', $pieces); 1240 | 1241 | return [$id_start, $id_start_ci, $id_name]; 1242 | } 1243 | } 1244 | --------------------------------------------------------------------------------