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

ClosureExporter::export()   F

Complexity

Conditions 29
Paths 150

Size

Total Lines 76
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 33.6241

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 29
eloc 50
c 2
b 1
f 0
nc 150
nop 2
dl 0
loc 76
ccs 42
cts 51
cp 0.8235
crap 33.6241
rs 3.75

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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