Passed
Push — master ( e0b1fd...93601b )
by Alexander
03:23 queued 01:00
created

ClosureExporter::processFullUse()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 2
nop 2
dl 0
loc 17
ccs 0
cts 10
cp 0
crap 20
rs 9.9332
c 0
b 0
f 0
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 7
    public function __construct()
38
    {
39 7
        $this->useStatementParser = new UseStatementParser();
40 7
    }
41
42
    /**
43
     * Export closure as a string containing PHP code.
44
     *
45
     * @param Closure $closure Closure to export.
46
     *
47
     * @throws ReflectionException
48
     *
49
     * @return string String containing PHP code.
50
     */
51 41
    public function export(Closure $closure): string
52
    {
53 41
        $reflection = new ReflectionFunction($closure);
54
55 41
        $fileName = $reflection->getFileName();
56 41
        $start = $reflection->getStartLine();
57 41
        $end = $reflection->getEndLine();
58
59 41
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
60
            return 'function () {/* Error: unable to determine Closure source */}';
61
        }
62
63 41
        --$start;
64 41
        $uses = $this->useStatementParser->fromFile($fileName);
65 41
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
66 41
        array_shift($tokens);
67
68 41
        $bufferUse = '';
69 41
        $closureTokens = [];
70 41
        $pendingParenthesisCount = 0;
71
72 41
        foreach ($tokens as $i => $token) {
73 41
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
74 41
                $closureTokens[] = $token[1];
75 41
                continue;
76
            }
77
78 41
            if ($closureTokens === []) {
79 41
                continue;
80
            }
81
82 41
            $readableToken = is_array($token) ? $token[1] : $token;
83
84 41
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
85 27
                if ($this->isUseConsistingOfMultipleParts($readableToken)) {
86
                    $readableToken = $this->processFullUse($readableToken, $uses);
87
                    $bufferUse = '';
88 27
                } elseif (isset($uses[$readableToken])) {
89 15
                    if (isset($tokens[$i + 2]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 2])) {
90 5
                        $bufferUse .= $uses[$readableToken];
91 5
                        continue;
92
                    }
93 15
                    $readableToken = $uses[$readableToken];
94
                }
95 27
                if (!empty($bufferUse)) {
96 5
                    $readableToken = '\\' . trim($readableToken, '\\');
97 5
                    if ($readableToken === '\\') {
98 5
                        continue;
99
                    }
100 5
                    if (isset($tokens[$i + 2]) && $this->useStatementParser->isTokenIsPartOfUse($tokens[$i + 2])) {
101 1
                        $bufferUse .= $readableToken;
102 1
                        continue;
103
                    }
104 5
                    if ($bufferUse !== $readableToken && strpos($readableToken, $bufferUse) === false) {
105 2
                        $readableToken = $bufferUse . $readableToken;
106
                    }
107 5
                    $bufferUse = '';
108
                }
109
            }
110
111 41
            if (is_string($token)) {
112 41
                if ($this->isOpenParenthesis($token)) {
113 41
                    $pendingParenthesisCount++;
114 41
                } elseif ($this->isCloseParenthesis($token)) {
115 41
                    if ($pendingParenthesisCount === 0) {
116 10
                        break;
117
                    }
118 41
                    $pendingParenthesisCount--;
119 36
                } elseif ($token === ',' || $token === ';') {
120 33
                    if ($pendingParenthesisCount === 0) {
121 31
                        break;
122
                    }
123
                }
124
            }
125
126 41
            $closureTokens[] = $readableToken;
127
        }
128
129 41
        return implode('', $closureTokens);
130
    }
131
132
    /**
133
     * Returns the last part from the use statement data.
134
     *
135
     * @param string $use The full use statement data.
136
     *
137
     * @return string The last part from the use statement data.
138
     */
139
    private function getUseLastPart(string $use): string
140
    {
141
        $parts = array_filter(explode('\\', $use));
142
        return (string) array_pop($parts);
143
    }
144
145
    /**
146
     * Processes and returns the full use statement data.
147
     *
148
     * @param string $use The use statement data to process.
149
     * @param array<string, string> $uses The use statement data.
150
     *
151
         * @return string The processed full use statement.
152
     */
153
    private function processFullUse(string $use, array $uses): string
154
    {
155
        $lastPart = $this->getUseLastPart($use);
156
157
        if (isset($uses[$lastPart])) {
158
            return $uses[$lastPart];
159
        }
160
161
        $result = '';
162
163
        do {
164
            $lastPart = $this->getUseLastPart($use);
165
            $use = mb_substr($use, 0, -mb_strlen("\\{$lastPart}"));
166
            $result = ($uses[$lastPart] ?? $lastPart) . '\\' . $result;
167
        } while (!empty($lastPart) && !isset($uses[$lastPart]));
168
169
        return '\\' . trim($result, '\\');
170
    }
171
172
    /**
173
     * Checks whether the use statement data consists of multiple parts.
174
     *
175
     * @param string $use The use statement data.
176
     *
177
     * @return bool Whether the use statement data consists of multiple parts.
178
     */
179 27
    private function isUseConsistingOfMultipleParts(string $use): bool
180
    {
181 27
        return $use !== '\\' && strpos($use, '\\') !== false;
182
    }
183
184
    /**
185
     * Checks whether the value of the token is an opening parenthesis.
186
     *
187
     * @param string $value The token value.
188
     *
189
     * @return bool Whether the value of the token is an opening parenthesis.
190
     */
191 41
    private function isOpenParenthesis(string $value): bool
192
    {
193 41
        return in_array($value, ['{', '[', '(']);
194
    }
195
196
    /**
197
     * Checks whether the value of the token is a closing parenthesis.
198
     *
199
     * @param string $value The token value.
200
     *
201
     * @return bool Whether the value of the token is a closing parenthesis.
202
     */
203 41
    private function isCloseParenthesis(string $value): bool
204
    {
205 41
        return in_array($value, ['}', ']', ')']);
206
    }
207
}
208