├── 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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------