Passed
Push — master ( e0e43e...d478f0 )
by Dmitriy
03:11 queued 02:35
created

ClosureExporter::export()   F

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 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 65
    public function export(Closure $closure, int $level = 0): string
53
    {
54 65
        $reflection = new ReflectionFunction($closure);
55
56 65
        $fileName = $reflection->getFileName();
57 65
        $start = $reflection->getStartLine();
58 65
        $end = $reflection->getEndLine();
59
60 65
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
61
            return 'function () {/* Error: unable to determine Closure source */}';
62
        }
63
64 65
        --$start;
65 65
        $uses = $this->useStatementParser->fromFile($fileName);
66 65
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
67 65
        array_shift($tokens);
68
69 65
        $bufferUse = '';
70 65
        $closureTokens = [];
71 65
        $pendingParenthesisCount = 0;
72
73
        /** @var int<1, max> $i */
74 65
        foreach ($tokens as $i => $token) {
75 65
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
76 65
                $closureTokens[] = $token[1];
77 65
                continue;
78
            }
79
80 65
            if ($closureTokens === []) {
81 65
                continue;
82
            }
83
84 65
            $readableToken = is_array($token) ? $token[1] : $token;
85
86 65
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
87 40
                if ($this->isUseConsistingOfMultipleParts($readableToken)) {
88 8
                    $readableToken = $this->processFullUse($readableToken, $uses);
89 8
                    $bufferUse = '';
90 40
                } elseif (isset($uses[$readableToken])) {
91 25
                    if (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
92
                        $bufferUse .= $uses[$readableToken];
93
                        continue;
94
                    }
95 25
                    $readableToken = $uses[$readableToken];
96 17
                } elseif ($readableToken === '\\' && isset($tokens[$i - 1][1]) && $tokens[$i - 1][1] === '\\') {
97
                    continue;
98 17
                } elseif (isset($tokens[$i + 1]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 1])) {
99
                    $bufferUse .= $readableToken;
100
                    continue;
101
                }
102 40
                if (!empty($bufferUse)) {
103
                    if ($bufferUse !== $readableToken && strpos($readableToken, $bufferUse) === false) {
104
                        $readableToken = $bufferUse . $readableToken;
105
                    }
106
                    $bufferUse = '';
107
                }
108
            }
109
110 65
            if (is_string($token)) {
111 65
                if ($this->isOpenParenthesis($token)) {
112 65
                    $pendingParenthesisCount++;
113 65
                } elseif ($this->isCloseParenthesis($token)) {
114 65
                    if ($pendingParenthesisCount === 0) {
115 14
                        break;
116
                    }
117 65
                    --$pendingParenthesisCount;
118 56
                } elseif ($token === ',' || $token === ';') {
119 53
                    if ($pendingParenthesisCount === 0) {
120 51
                        break;
121
                    }
122
                }
123
            }
124
125 65
            $closureTokens[] = $readableToken;
126
        }
127
128 65
        return $this->formatClosure(implode('', $closureTokens), $level);
129
    }
130
131 65
    private function formatClosure(string $code, int $level): string
132
    {
133 65
        $code = explode("\n", $code);
134 65
        $fistLine = array_shift($code);
135 65
        $minimumIndent = null;
136 65
        for ($i = 1; $i <= count($code) - 1; $i++) {
137 13
            $indent = strspn($code[$i], ' ');
138 13
            if ($indent === mb_strlen($code[$i])) {
139
                continue;
140
            }
141 13
            if ($minimumIndent === null || $indent < $minimumIndent) {
142 13
                $minimumIndent = $indent;
143
            }
144
        }
145 65
        if ($minimumIndent > 0) {
146 13
            foreach ($code as $index => $line) {
147 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

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