Passed
Pull Request — master (#54)
by Alexander
02:12
created

ClosureExporter::formatClosure()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 9
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 16
ccs 10
cts 10
cp 1
crap 4
rs 9.9666
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
     * @param int $level Level for padding.
47
     *
48
     * @throws ReflectionException
49
     *
50
     * @return string String containing PHP code.
51
     */
52 44
    public function export(Closure $closure, int $level = 0): string
53
    {
54 44
        $reflection = new ReflectionFunction($closure);
55
56 44
        $fileName = $reflection->getFileName();
57 44
        $start = $reflection->getStartLine();
58 44
        $end = $reflection->getEndLine();
59
60 44
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
61
            return 'function () {/* Error: unable to determine Closure source */}';
62
        }
63
64 44
        --$start;
65 44
        $uses = $this->useStatementParser->fromFile($fileName);
66 44
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
67 44
        array_shift($tokens);
68
69 44
        $bufferUse = '';
70 44
        $closureTokens = [];
71 44
        $pendingParenthesisCount = 0;
72
73 44
        foreach ($tokens as $i => $token) {
74 44
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
75 44
                $closureTokens[] = $token[1];
76 44
                continue;
77
            }
78
79 44
            if ($closureTokens === []) {
80 44
                continue;
81
            }
82
83 44
            $readableToken = is_array($token) ? $token[1] : $token;
84
85 44
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
86 30
                if ($this->isUseConsistingOfMultipleParts($readableToken)) {
87
                    $readableToken = $this->processFullUse($readableToken, $uses);
88
                    $bufferUse = '';
89 30
                } elseif (isset($uses[$readableToken])) {
90 17
                    if (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
91 5
                        $bufferUse .= $uses[$readableToken];
92 5
                        continue;
93
                    }
94 17
                    $readableToken = $uses[$readableToken];
95 21
                } elseif ($readableToken === '\\' && isset($tokens[$i - 1][1]) && $tokens[$i - 1][1] === '\\') {
96
                    continue;
97 21
                } elseif (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
98 6
                    $bufferUse .= $readableToken;
99 6
                    continue;
100
                }
101 30
                if (!empty($bufferUse)) {
102 6
                    if ($bufferUse !== $readableToken && strpos($readableToken, $bufferUse) === false) {
103 2
                        $readableToken = $bufferUse . $readableToken;
104
                    }
105 6
                    $bufferUse = '';
106
                }
107
            }
108
109 44
            if (is_string($token)) {
110 44
                if ($this->isOpenParenthesis($token)) {
111 44
                    $pendingParenthesisCount++;
112 44
                } elseif ($this->isCloseParenthesis($token)) {
113 44
                    if ($pendingParenthesisCount === 0) {
114 12
                        break;
115
                    }
116 44
                    $pendingParenthesisCount--;
117 37
                } elseif ($token === ',' || $token === ';') {
118 34
                    if ($pendingParenthesisCount === 0) {
119 32
                        break;
120
                    }
121
                }
122
            }
123
124 44
            $closureTokens[] = $readableToken;
125
        }
126
127 44
        return $this->formatClosure(implode('', $closureTokens), $level);
128
    }
129
130 44
    private function formatClosure(string $code, int $level): string
131
    {
132 44
        if ($level <= 0) {
133 37
            return $code;
134
        }
135 7
        $spaces = str_repeat(' ', ($level -1) * 4);
136 7
        $lines = explode("\n", $code);
137
138 7
        foreach ($lines as $index => $line) {
139 7
            if ($index === 0) {
140 7
                continue;
141
            }
142 1
            $lines[$index] = $spaces . $line;
143
        }
144
145 7
        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
    private function getUseLastPart(string $use): string
156
    {
157
        $parts = array_filter(explode('\\', $use));
158
        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
    private function processFullUse(string $use, array $uses): string
170
    {
171
        $lastPart = $this->getUseLastPart($use);
172
173
        if (isset($uses[$lastPart])) {
174
            return $uses[$lastPart];
175
        }
176
177
        $result = '';
178
179
        do {
180
            $lastPart = $this->getUseLastPart($use);
181
            $use = mb_substr($use, 0, -mb_strlen("\\{$lastPart}"));
182
            $result = ($uses[$lastPart] ?? $lastPart) . '\\' . $result;
183
        } while (!empty($lastPart) && !isset($uses[$lastPart]));
184
185
        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 30
    private function isUseConsistingOfMultipleParts(string $use): bool
196
    {
197 30
        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 44
    private function isOpenParenthesis(string $value): bool
208
    {
209 44
        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 44
    private function isCloseParenthesis(string $value): bool
220
    {
221 44
        return in_array($value, ['}', ']', ')']);
222
    }
223
}
224