Passed
Pull Request — master (#45)
by Evgeniy
01:58
created

ClosureExporter::export()   F

Complexity

Conditions 26
Paths 194

Size

Total Lines 80
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 52
CRAP Score 26.0341

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 26
eloc 53
c 2
b 1
f 0
nc 194
nop 1
dl 0
loc 80
ccs 52
cts 54
cp 0.963
crap 26.0341
rs 3.3833

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_string;
20
use function strpos;
21
use function token_get_all;
22
23
/**
24
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
25
 *
26
 * The string is a valid PHP expression that can be evaluated by PHP parser
27
 * and the evaluation result will give back the closure instance.
28
 */
29
final class ClosureExporter
30
{
31
    private UseStatementParser $useStatementParser;
32
33 6
    public function __construct()
34
    {
35 6
        $this->useStatementParser = new UseStatementParser();
36 6
    }
37
38
    /**
39
     * Export closure as a string containing PHP code.
40
     *
41
     * @param Closure $closure Closure to export.
42
     *
43
     * @throws ReflectionException
44
     *
45
     * @return string String containing PHP code.
46
     */
47 34
    public function export(Closure $closure): string
48
    {
49 34
        $reflection = new ReflectionFunction($closure);
50
51 34
        $fileName = $reflection->getFileName();
52 34
        $start = $reflection->getStartLine();
53 34
        $end = $reflection->getEndLine();
54
55 34
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
56
            return 'function () {/* Error: unable to determine Closure source */}';
57
        }
58
59 34
        --$start;
60 34
        $uses = $this->useStatementParser->fromFile($fileName);
61 34
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
62 34
        array_shift($tokens);
63
64 34
        $buffer = '';
65 34
        $closureTokens = [];
66 34
        $previousUsePart = '';
67 34
        $pendingParenthesisCount = 0;
68
69 34
        foreach ($tokens as $token) {
70 34
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
71 34
                $closureTokens[] = $token[1];
72 34
                continue;
73
            }
74
75 34
            if ($closureTokens === []) {
76 34
                continue;
77
            }
78
79 34
            $readableToken = is_array($token) ? $token[1] : $token;
80
81 34
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
82 20
                $buffer .= $token[1];
83 20
                if (PHP_VERSION_ID >= 80000 && $this->isUseConsistingOfMultipleParts($buffer)) {
84
                    $buffer = $this->getUseLastPart($buffer);
85
                }
86 20
                if (!empty($previousUsePart) && $buffer === '\\') {
87 3
                    continue;
88
                }
89 20
                if (isset($uses[$buffer])) {
90 9
                    if ($this->isNotFullUseAlias($buffer, $uses)) {
91 3
                        $previousUsePart = $uses[$buffer];
92 3
                        $buffer = '';
93 3
                        continue;
94
                    }
95 6
                    $readableToken = (empty($previousUsePart) || strpos($uses[$buffer], $previousUsePart) === false)
96 6
                        ? $previousUsePart . $uses[$buffer]
97 6
                        : $uses[$buffer]
98
                    ;
99 6
                    $buffer = '';
100 6
                    $previousUsePart = '';
101 20
                } elseif (isset($uses[$token[1]])) {
102 3
                    $readableToken = $uses[$token[1]];
103 3
                    $previousUsePart = '';
104 3
                    $buffer = '';
105
                }
106
            }
107
108 34
            if (is_string($token)) {
109 34
                if ($this->isOpenParenthesis($token)) {
110 34
                    $pendingParenthesisCount++;
111 34
                } elseif ($this->isCloseParenthesis($token)) {
112 34
                    if ($pendingParenthesisCount === 0) {
113 7
                        break;
114
                    }
115 34
                    $pendingParenthesisCount--;
116 31
                } elseif ($token === ',' || $token === ';') {
117 29
                    if ($pendingParenthesisCount === 0) {
118 27
                        break;
119
                    }
120
                }
121
            }
122
123 34
            $closureTokens[] = $readableToken;
124
        }
125
126 34
        return implode('', $closureTokens);
127
    }
128
129
    /**
130
     * Returns the last part from the use statement data.
131
     *
132
     * @param string $use The full use statement data.
133
     *
134
     * @return string The last part from the use statement data.
135
     */
136 9
    private function getUseLastPart(string $use): string
137
    {
138 9
        $parts = array_filter(explode('\\', $use));
139 9
        return array_pop($parts);
140
    }
141
142
    /**
143
     * Checks whether the use statement data consists of multiple parts.
144
     *
145
     * @param string $use The use statement data.
146
     *
147
     * @return bool Whether the use statement data consists of multiple parts.
148
     */
149
    private function isUseConsistingOfMultipleParts(string $use): bool
150
    {
151
        return $use !== '\\' && strpos($use, '\\') !== false;
152
    }
153
154
    /**
155
     * Checks whether the use statement data is not a full use statement data alias.
156
     *
157
     * @param string $useKey The use statement data key.
158
     * @param array<string, string> $uses The use statement data.
159
     *
160
     * @return bool Whether the use statement data is not a full use statement data alias.
161
     */
162 9
    private function isNotFullUseAlias(string $useKey, array $uses): bool
163
    {
164 9
        if (!isset($uses[$useKey])) {
165
            return false;
166
        }
167
168 9
        $lastPart = $this->getUseLastPart($uses[$useKey]);
169 9
        return isset($uses[$lastPart]) && $uses[$lastPart] !== $uses[$useKey];
170
    }
171
172
173
    /**
174
     * Checks whether the value of the token is an opening parenthesis.
175
     *
176
     * @param string $value The token value.
177
     *
178
     * @return bool Whether the value of the token is an opening parenthesis.
179
     */
180 34
    private function isOpenParenthesis(string $value): bool
181
    {
182 34
        return in_array($value, ['{', '[', '(']);
183
    }
184
185
    /**
186
     * Checks whether the value of the token is a closing parenthesis.
187
     *
188
     * @param string $value The token value.
189
     *
190
     * @return bool Whether the value of the token is a closing parenthesis.
191
     */
192 34
    private function isCloseParenthesis(string $value): bool
193
    {
194 34
        return in_array($value, ['}', ']', ')']);
195
    }
196
}
197