ClosureExporter::export()   F
last analyzed

Complexity

Conditions 29
Paths 150

Size

Total Lines 77
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 33.6241

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 29
eloc 50
c 3
b 1
f 0
nc 150
nop 2
dl 0
loc 77
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 token_get_all;
24
use function trim;
25
26
/**
27
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
28
 *
29
 * The string is a valid PHP expression that can be evaluated by PHP parser
30
 * and the evaluation result will give back the closure instance.
31
 */
32
final class ClosureExporter
33
{
34
    private UseStatementParser $useStatementParser;
35
36 10
    public function __construct()
37
    {
38 10
        $this->useStatementParser = new UseStatementParser();
39
    }
40
41
    /**
42
     * Export closure as a string containing PHP code.
43
     *
44
     * @param Closure $closure Closure to export.
45
     * @param int $level Level for padding.
46
     *
47
     * @throws ReflectionException
48
     *
49
     * @return string String containing PHP code.
50
     */
51 65
    public function export(Closure $closure, int $level = 0): string
52
    {
53 65
        $reflection = new ReflectionFunction($closure);
54
55 65
        $fileName = $reflection->getFileName();
56 65
        $start = $reflection->getStartLine();
57 65
        $end = $reflection->getEndLine();
58
59 65
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
60
            return 'function () {/* Error: unable to determine Closure source */}';
61
        }
62
63 65
        --$start;
64 65
        $uses = $this->useStatementParser->fromFile($fileName);
65 65
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
66 65
        array_shift($tokens);
67
68 65
        $bufferUse = '';
69 65
        $closureTokens = [];
70 65
        $pendingParenthesisCount = 0;
71
72
        /** @var int<1, max> $i */
73 65
        foreach ($tokens as $i => $token) {
74 65
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
75 65
                $closureTokens[] = $token[1];
76 65
                continue;
77
            }
78
79 65
            if ($closureTokens === []) {
80 65
                continue;
81
            }
82
83 65
            $readableToken = is_array($token) ? $token[1] : $token;
84
85 65
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
86 40
                if ($this->isUseConsistingOfMultipleParts($readableToken)) {
87 8
                    $readableToken = $this->processFullUse($readableToken, $uses);
88 8
                    $bufferUse = '';
89 40
                } elseif (isset($uses[$readableToken])) {
90 25
                    if (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
91
                        $bufferUse .= $uses[$readableToken];
92
                        continue;
93
                    }
94 25
                    $readableToken = $uses[$readableToken];
95 17
                } elseif ($readableToken === '\\' && isset($tokens[$i - 1][1]) && $tokens[$i - 1][1] === '\\') {
96
                    continue;
97 17
                } elseif (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
98
                    $bufferUse .= $readableToken;
99
                    continue;
100
                }
101 40
                if (!empty($bufferUse)) {
102
                    if ($bufferUse !== $readableToken && !str_contains($readableToken, $bufferUse)) {
103
                        $readableToken = $bufferUse . $readableToken;
104
                    }
105
                    $bufferUse = '';
106
                }
107
            }
108
109 65
            if (is_string($token)) {
110 65
                if ($this->isOpenParenthesis($token)) {
111 65
                    $pendingParenthesisCount++;
112 65
                } elseif ($this->isCloseParenthesis($token)) {
113 65
                    if ($pendingParenthesisCount === 0) {
114 14
                        break;
115
                    }
116 65
                    --$pendingParenthesisCount;
117 56
                } elseif ($token === ',' || $token === ';') {
118 53
                    if ($pendingParenthesisCount === 0) {
119 51
                        break;
120
                    }
121
                }
122
            }
123
124 65
            $closureTokens[] = $readableToken;
125
        }
126
127 65
        return $this->formatClosure(implode('', $closureTokens), $level);
128
    }
129
130 65
    private function formatClosure(string $code, int $level): string
131
    {
132 65
        $code = explode("\n", $code);
133 65
        $fistLine = array_shift($code);
134 65
        $minimumIndent = null;
135 65
        for ($i = 1; $i <= count($code) - 1; $i++) {
136 13
            $indent = strspn($code[$i], ' ');
137 13
            if ($indent === mb_strlen($code[$i])) {
138
                continue;
139
            }
140 13
            if ($minimumIndent === null || $indent < $minimumIndent) {
141 13
                $minimumIndent = $indent;
142
            }
143
        }
144 65
        if ($minimumIndent > 0) {
145 13
            foreach ($code as $index => $line) {
146 13
                $code[$index] = mb_substr($line, $minimumIndent);
0 ignored issues
show
Bug introduced by
$minimumIndent of type null is incompatible with the type integer expected by parameter $start of mb_substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

146
                $code[$index] = mb_substr($line, /** @scrutinizer ignore-type */ $minimumIndent);
Loading history...
147
            }
148
        }
149
150 65
        $spaces = $level <= 1 ? '' : str_repeat(' ', ($level - 1) * 4);
151 65
        return implode("\n" . $spaces, [$fistLine, ...$code]);
152
    }
153
154
    /**
155
     * Returns the last part from the use statement data.
156
     *
157
     * @param string $use The full use statement data.
158
     *
159
     * @return string The last part from the use statement data.
160
     */
161 8
    private function getUseLastPart(string $use): string
162
    {
163 8
        $parts = array_filter(explode('\\', $use));
164 8
        return (string) array_pop($parts);
165
    }
166
167
    /**
168
     * Processes and returns the full use statement data.
169
     *
170
     * @param string $use The use statement data to process.
171
     * @param array<string, string> $uses The use statement data.
172
     *
173
         * @return string The processed full use statement.
174
     */
175 8
    private function processFullUse(string $use, array $uses): string
176
    {
177 8
        $lastPart = $this->getUseLastPart($use);
178
179 8
        if (isset($uses[$lastPart])) {
180 6
            return $uses[$lastPart];
181
        }
182
183 2
        $result = '';
184
185
        do {
186 2
            $lastPart = $this->getUseLastPart($use);
187 2
            $use = mb_substr($use, 0, -mb_strlen("\\{$lastPart}"));
188 2
            $result = ($uses[$lastPart] ?? $lastPart) . '\\' . $result;
189 2
        } while (!empty($lastPart) && !isset($uses[$lastPart]));
190
191 2
        return '\\' . trim($result, '\\');
192
    }
193
194
    /**
195
     * Checks whether the use statement data consists of multiple parts.
196
     *
197
     * @param string $use The use statement data.
198
     *
199
     * @return bool Whether the use statement data consists of multiple parts.
200
     */
201 40
    private function isUseConsistingOfMultipleParts(string $use): bool
202
    {
203 40
        return $use !== '\\' && str_contains($use, '\\');
204
    }
205
206
    /**
207
     * Checks whether the value of the token is an opening parenthesis.
208
     *
209
     * @param string $value The token value.
210
     *
211
     * @return bool Whether the value of the token is an opening parenthesis.
212
     */
213 65
    private function isOpenParenthesis(string $value): bool
214
    {
215 65
        return in_array($value, ['{', '[', '(']);
216
    }
217
218
    /**
219
     * Checks whether the value of the token is a closing parenthesis.
220
     *
221
     * @param string $value The token value.
222
     *
223
     * @return bool Whether the value of the token is a closing parenthesis.
224
     */
225 65
    private function isCloseParenthesis(string $value): bool
226
    {
227 65
        return in_array($value, ['}', ']', ')']);
228
    }
229
}
230