Passed
Pull Request — master (#54)
by Alexander
02:12
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
     * @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