Passed
Push — master ( fb5594...a56e1d )
by Alexander
03:25 queued 57s
created

ClosureExporter   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 102
Duplicated Lines 0 %

Test Coverage

Coverage 93.22%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 58
c 2
b 1
f 0
dl 0
loc 102
ccs 55
cts 59
cp 0.9322
rs 9.6
wmc 35

3 Methods

Rating   Name   Duplication   Size   Complexity  
B isNextTokenIsPartOfNamespace() 0 11 9
A __construct() 0 3 1
D export() 0 71 25
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\VarDumper;
6
7
use Closure;
8
use ReflectionException;
9
use ReflectionFunction;
10
11
use function array_key_exists;
12
use function array_filter;
13
use function array_pop;
14
use function array_shift;
15
use function array_slice;
16
use function defined;
17
use function explode;
18
use function implode;
19
use function in_array;
20
use function is_array;
21
use function strpos;
22
use function token_get_all;
23
24
/**
25
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
26
 *
27
 * The string is a valid PHP expression that can be evaluated by PHP parser
28
 * and the evaluation result will give back the closure instance.
29
 */
30
final class ClosureExporter
31
{
32
    private UseStatementParser $useStatementParser;
33
34 1
    public function __construct()
35
    {
36 1
        $this->useStatementParser = new UseStatementParser();
37 1
    }
38
39
    /**
40
     * Export closure as a string containing PHP code.
41
     *
42
     * @param Closure $closure Closure to export.
43
     *
44
     * @throws ReflectionException
45
     *
46
     * @return string String containing PHP code.
47
     */
48 39
    public function export(Closure $closure): string
49
    {
50 39
        $reflection = new ReflectionFunction($closure);
51
52 39
        $fileName = $reflection->getFileName();
53 39
        $start = $reflection->getStartLine();
54 39
        $end = $reflection->getEndLine();
55
56 39
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
57
            return 'function() {/* Error: unable to determine Closure source */}';
58
        }
59
60 39
        --$start;
61 39
        $uses = $this->useStatementParser->fromFile($fileName);
62
63 39
        $source = implode('', array_slice($fileContent, $start, $end - $start));
64 39
        $tokens = token_get_all('<?php ' . $source);
65 39
        array_shift($tokens);
66
67 39
        $closureTokens = [];
68 39
        $pendingParenthesisCount = 0;
69 39
        $isShortClosure = false;
70 39
        $buffer = '';
71 39
        foreach ($tokens as $token) {
72 39
            if (!isset($token[0])) {
73
                continue;
74
            }
75 39
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
76 39
                $closureTokens[] = $token[1];
77 39
                if (!$isShortClosure && $token[0] === T_FN) {
78 31
                    $isShortClosure = true;
79
                }
80 39
                continue;
81
            }
82 39
            if ($closureTokens !== []) {
83 39
                $readableToken = $token[1] ?? $token;
84 39
                if ($this->isNextTokenIsPartOfNamespace($token)) {
85 20
                    $buffer .= $token[1];
86
                    if (
87 20
                        PHP_VERSION_ID >= 80000
88 20
                        && $buffer !== '\\'
89 20
                        && strpos($buffer, '\\') !== false
90
                    ) {
91
                        $usesKeys = array_filter(explode('\\', $buffer));
92
                        $buffer = array_pop($usesKeys);
93
                    }
94
                    if (
95 20
                        array_key_exists($buffer, $uses)
96 20
                        && !$this->isNextTokenIsPartOfNamespace(next($tokens))
97
                    ) {
98 12
                        $readableToken = $uses[$buffer];
99 12
                        $buffer = '';
100
                    }
101
                }
102 39
                if ($token === '{' || $token === '[') {
103 12
                    $pendingParenthesisCount++;
104 39
                } elseif ($token === '}' || $token === ']') {
105 15
                    if ($pendingParenthesisCount === 0) {
106 3
                        break;
107
                    }
108 12
                    $pendingParenthesisCount--;
109 39
                } elseif ($token === ',' || $token === ';') {
110 36
                    if ($pendingParenthesisCount === 0) {
111 36
                        break;
112
                    }
113
                }
114 39
                $closureTokens[] = $readableToken;
115
            }
116
        }
117
118 39
        return implode('', $closureTokens);
119
    }
120
121 39
    private function isNextTokenIsPartOfNamespace($token): bool
122
    {
123 39
        if (!is_array($token)) {
124 39
            return false;
125
        }
126
127 39
        return $token[0] === T_STRING
128 39
            || $token[0] === T_NS_SEPARATOR
129 39
            || (defined('T_NAME_QUALIFIED') && $token[0] === T_NAME_QUALIFIED)
130 39
            || (defined('T_NAME_FULLY_QUALIFIED') && $token[0] === T_NAME_FULLY_QUALIFIED)
131 39
            || (defined('T_NAME_RELATIVE') && $token[0] === T_NAME_RELATIVE);
132
    }
133
}
134