Passed
Push — master ( a53302...e71c1f )
by Alexander
06:09 queued 03:53
created

ClosureExporter::isUseConsistingOfMultipleParts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
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_array;
20
use function is_string;
21
use function mb_strlen;
22
use function mb_substr;
23
use function strpos;
24
use function token_get_all;
25
use function trim;
26
27
/**
28
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
29
 *
30
 * The string is a valid PHP expression that can be evaluated by PHP parser
31
 * and the evaluation result will give back the closure instance.
32
 */
33
final class ClosureExporter
34
{
35
    private UseStatementParser $useStatementParser;
36
37 10
    public function __construct()
38
    {
39 10
        $this->useStatementParser = new UseStatementParser();
40
    }
41
42
    /**
43
     * Export closure as a string containing PHP code.
44
     *
45
     * @param Closure $closure Closure to export.
46
     * @param int $level Level for padding.
47
     *
48
     * @throws ReflectionException
49
     *
50
     * @return string String containing PHP code.
51
     */
52 55
    public function export(Closure $closure, int $level = 0): string
53
    {
54 55
        $reflection = new ReflectionFunction($closure);
55
56 55
        $fileName = $reflection->getFileName();
57 55
        $start = $reflection->getStartLine();
58 55
        $end = $reflection->getEndLine();
59
60 55
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
61
            return 'function () {/* Error: unable to determine Closure source */}';
62
        }
63
64 55
        --$start;
65 55
        $uses = $this->useStatementParser->fromFile($fileName);
66 55
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
67 55
        array_shift($tokens);
68
69 55
        $bufferUse = '';
70 55
        $closureTokens = [];
71 55
        $pendingParenthesisCount = 0;
72
73 55
        foreach ($tokens as $i => $token) {
74 55
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
75 55
                $closureTokens[] = $token[1];
76 55
                continue;
77
            }
78
79 55
            if ($closureTokens === []) {
80 55
                continue;
81
            }
82
83 55
            $readableToken = is_array($token) ? $token[1] : $token;
84
85 55
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
86 35
                if ($this->isUseConsistingOfMultipleParts($readableToken)) {
87 7
                    $readableToken = $this->processFullUse($readableToken, $uses);
88 7
                    $bufferUse = '';
89 35
                } elseif (isset($uses[$readableToken])) {
90 21
                    if (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
91
                        $bufferUse .= $uses[$readableToken];
92
                        continue;
93
                    }
94 21
                    $readableToken = $uses[$readableToken];
95 16
                } elseif ($readableToken === '\\' && isset($tokens[$i - 1][1]) && $tokens[$i - 1][1] === '\\') {
96
                    continue;
97 16
                } elseif (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
98
                    $bufferUse .= $readableToken;
99
                    continue;
100
                }
101 35
                if (!empty($bufferUse)) {
102
                    if ($bufferUse !== $readableToken && strpos($readableToken, $bufferUse) === false) {
103
                        $readableToken = $bufferUse . $readableToken;
104
                    }
105
                    $bufferUse = '';
106
                }
107
            }
108
109 55
            if (is_string($token)) {
110 55
                if ($this->isOpenParenthesis($token)) {
111 55
                    $pendingParenthesisCount++;
112 55
                } elseif ($this->isCloseParenthesis($token)) {
113 55
                    if ($pendingParenthesisCount === 0) {
114 13
                        break;
115
                    }
116 55
                    $pendingParenthesisCount--;
117 47
                } elseif ($token === ',' || $token === ';') {
118 44
                    if ($pendingParenthesisCount === 0) {
119 42
                        break;
120
                    }
121
                }
122
            }
123
124 55
            $closureTokens[] = $readableToken;
125
        }
126
127 55
        return $this->formatClosure(implode('', $closureTokens), $level);
128
    }
129
130 55
    private function formatClosure(string $code, int $level): string
131
    {
132 55
        if ($level <= 0) {
133 47
            return $code;
134
        }
135 8
        $spaces = str_repeat(' ', ($level -1) * 4);
136 8
        $lines = explode("\n", $code);
137
138 8
        foreach ($lines as $index => $line) {
139 8
            if ($index === 0) {
140 8
                continue;
141
            }
142 1
            $lines[$index] = $spaces . $line;
143
        }
144
145 8
        return rtrim(implode('', $lines), "\n");
146
    }
147
148
    /**
149
     * Returns the last part from the use statement data.
150
     *
151
     * @param string $use The full use statement data.
152
     *
153
     * @return string The last part from the use statement data.
154
     */
155 7
    private function getUseLastPart(string $use): string
156
    {
157 7
        $parts = array_filter(explode('\\', $use));
158 7
        return (string) array_pop($parts);
159
    }
160
161
    /**
162
     * Processes and returns the full use statement data.
163
     *
164
     * @param string $use The use statement data to process.
165
     * @param array<string, string> $uses The use statement data.
166
     *
167
         * @return string The processed full use statement.
168
     */
169 7
    private function processFullUse(string $use, array $uses): string
170
    {
171 7
        $lastPart = $this->getUseLastPart($use);
172
173 7
        if (isset($uses[$lastPart])) {
174 5
            return $uses[$lastPart];
175
        }
176
177 2
        $result = '';
178
179
        do {
180 2
            $lastPart = $this->getUseLastPart($use);
181 2
            $use = mb_substr($use, 0, -mb_strlen("\\{$lastPart}"));
182 2
            $result = ($uses[$lastPart] ?? $lastPart) . '\\' . $result;
183 2
        } while (!empty($lastPart) && !isset($uses[$lastPart]));
184
185 2
        return '\\' . trim($result, '\\');
186
    }
187
188
    /**
189
     * Checks whether the use statement data consists of multiple parts.
190
     *
191
     * @param string $use The use statement data.
192
     *
193
     * @return bool Whether the use statement data consists of multiple parts.
194
     */
195 35
    private function isUseConsistingOfMultipleParts(string $use): bool
196
    {
197 35
        return $use !== '\\' && strpos($use, '\\') !== false;
198
    }
199
200
    /**
201
     * Checks whether the value of the token is an opening parenthesis.
202
     *
203
     * @param string $value The token value.
204
     *
205
     * @return bool Whether the value of the token is an opening parenthesis.
206
     */
207 55
    private function isOpenParenthesis(string $value): bool
208
    {
209 55
        return in_array($value, ['{', '[', '(']);
210
    }
211
212
    /**
213
     * Checks whether the value of the token is a closing parenthesis.
214
     *
215
     * @param string $value The token value.
216
     *
217
     * @return bool Whether the value of the token is a closing parenthesis.
218
     */
219 55
    private function isCloseParenthesis(string $value): bool
220
    {
221 55
        return in_array($value, ['}', ']', ')']);
222
    }
223
}
224