ClosureExporter   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 88.51%

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 81
dl 0
loc 196
ccs 77
cts 87
cp 0.8851
rs 8.64
c 4
b 2
f 0
wmc 47

8 Methods

Rating   Name   Duplication   Size   Complexity  
A isCloseParenthesis() 0 3 1
B formatClosure() 0 22 8
A processFullUse() 0 17 4
A getUseLastPart() 0 4 1
A isOpenParenthesis() 0 3 1
A isUseConsistingOfMultipleParts() 0 3 2
A __construct() 0 3 1
F export() 0 77 29

How to fix   Complexity   

Complex Class

Complex classes like ClosureExporter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ClosureExporter, and based on these observations, apply Extract Interface, too.

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