Passed
Pull Request — master (#45)
by Evgeniy
01:58
created

ClosureExporter   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 166
Duplicated Lines 0 %

Test Coverage

Coverage 92.96%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 64
c 2
b 1
f 0
dl 0
loc 166
ccs 66
cts 71
cp 0.9296
rs 9.6
wmc 35

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A isCloseParenthesis() 0 3 1
A isNotFullUseAlias() 0 8 3
A getUseLastPart() 0 4 1
A isOpenParenthesis() 0 3 1
A isUseConsistingOfMultipleParts() 0 3 2
F export() 0 80 26
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_filter;
12
use function array_pop;
13
use function array_shift;
14
use function array_slice;
15
use function explode;
16
use function file;
17
use function implode;
18
use function in_array;
19
use function is_string;
20
use function strpos;
21
use function token_get_all;
22
23
/**
24
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
25
 *
26
 * The string is a valid PHP expression that can be evaluated by PHP parser
27
 * and the evaluation result will give back the closure instance.
28
 */
29
final class ClosureExporter
30
{
31
    private UseStatementParser $useStatementParser;
32
33 6
    public function __construct()
34
    {
35 6
        $this->useStatementParser = new UseStatementParser();
36 6
    }
37
38
    /**
39
     * Export closure as a string containing PHP code.
40
     *
41
     * @param Closure $closure Closure to export.
42
     *
43
     * @throws ReflectionException
44
     *
45
     * @return string String containing PHP code.
46
     */
47 34
    public function export(Closure $closure): string
48
    {
49 34
        $reflection = new ReflectionFunction($closure);
50
51 34
        $fileName = $reflection->getFileName();
52 34
        $start = $reflection->getStartLine();
53 34
        $end = $reflection->getEndLine();
54
55 34
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
56
            return 'function () {/* Error: unable to determine Closure source */}';
57
        }
58
59 34
        --$start;
60 34
        $uses = $this->useStatementParser->fromFile($fileName);
61 34
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
62 34
        array_shift($tokens);
63
64 34
        $buffer = '';
65 34
        $closureTokens = [];
66 34
        $previousUsePart = '';
67 34
        $pendingParenthesisCount = 0;
68
69 34
        foreach ($tokens as $token) {
70 34
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
71 34
                $closureTokens[] = $token[1];
72 34
                continue;
73
            }
74
75 34
            if ($closureTokens === []) {
76 34
                continue;
77
            }
78
79 34
            $readableToken = is_array($token) ? $token[1] : $token;
80
81 34
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
82 20
                $buffer .= $token[1];
83 20
                if (PHP_VERSION_ID >= 80000 && $this->isUseConsistingOfMultipleParts($buffer)) {
84
                    $buffer = $this->getUseLastPart($buffer);
85
                }
86 20
                if (!empty($previousUsePart) && $buffer === '\\') {
87 3
                    continue;
88
                }
89 20
                if (isset($uses[$buffer])) {
90 9
                    if ($this->isNotFullUseAlias($buffer, $uses)) {
91 3
                        $previousUsePart = $uses[$buffer];
92 3
                        $buffer = '';
93 3
                        continue;
94
                    }
95 6
                    $readableToken = (empty($previousUsePart) || strpos($uses[$buffer], $previousUsePart) === false)
96 6
                        ? $previousUsePart . $uses[$buffer]
97 6
                        : $uses[$buffer]
98
                    ;
99 6
                    $buffer = '';
100 6
                    $previousUsePart = '';
101 20
                } elseif (isset($uses[$token[1]])) {
102 3
                    $readableToken = $uses[$token[1]];
103 3
                    $previousUsePart = '';
104 3
                    $buffer = '';
105
                }
106
            }
107
108 34
            if (is_string($token)) {
109 34
                if ($this->isOpenParenthesis($token)) {
110 34
                    $pendingParenthesisCount++;
111 34
                } elseif ($this->isCloseParenthesis($token)) {
112 34
                    if ($pendingParenthesisCount === 0) {
113 7
                        break;
114
                    }
115 34
                    $pendingParenthesisCount--;
116 31
                } elseif ($token === ',' || $token === ';') {
117 29
                    if ($pendingParenthesisCount === 0) {
118 27
                        break;
119
                    }
120
                }
121
            }
122
123 34
            $closureTokens[] = $readableToken;
124
        }
125
126 34
        return implode('', $closureTokens);
127
    }
128
129
    /**
130
     * Returns the last part from the use statement data.
131
     *
132
     * @param string $use The full use statement data.
133
     *
134
     * @return string The last part from the use statement data.
135
     */
136 9
    private function getUseLastPart(string $use): string
137
    {
138 9
        $parts = array_filter(explode('\\', $use));
139 9
        return array_pop($parts);
140
    }
141
142
    /**
143
     * Checks whether the use statement data consists of multiple parts.
144
     *
145
     * @param string $use The use statement data.
146
     *
147
     * @return bool Whether the use statement data consists of multiple parts.
148
     */
149
    private function isUseConsistingOfMultipleParts(string $use): bool
150
    {
151
        return $use !== '\\' && strpos($use, '\\') !== false;
152
    }
153
154
    /**
155
     * Checks whether the use statement data is not a full use statement data alias.
156
     *
157
     * @param string $useKey The use statement data key.
158
     * @param array<string, string> $uses The use statement data.
159
     *
160
     * @return bool Whether the use statement data is not a full use statement data alias.
161
     */
162 9
    private function isNotFullUseAlias(string $useKey, array $uses): bool
163
    {
164 9
        if (!isset($uses[$useKey])) {
165
            return false;
166
        }
167
168 9
        $lastPart = $this->getUseLastPart($uses[$useKey]);
169 9
        return isset($uses[$lastPart]) && $uses[$lastPart] !== $uses[$useKey];
170
    }
171
172
173
    /**
174
     * Checks whether the value of the token is an opening parenthesis.
175
     *
176
     * @param string $value The token value.
177
     *
178
     * @return bool Whether the value of the token is an opening parenthesis.
179
     */
180 34
    private function isOpenParenthesis(string $value): bool
181
    {
182 34
        return in_array($value, ['{', '[', '(']);
183
    }
184
185
    /**
186
     * Checks whether the value of the token is a closing parenthesis.
187
     *
188
     * @param string $value The token value.
189
     *
190
     * @return bool Whether the value of the token is a closing parenthesis.
191
     */
192 34
    private function isCloseParenthesis(string $value): bool
193
    {
194 34
        return in_array($value, ['}', ']', ')']);
195
    }
196
}
197