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

ClosureExporter::export()   F

Complexity

Conditions 29
Paths 150

Size

Total Lines 76
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 29.4052

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 47
cts 51
cp 0.9216
crap 29.4052
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 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