Passed
Pull Request — master (#54)
by Dmitriy
07:27
created

ClosureExporter::processFullUse()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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