Passed
Pull Request — master (#50)
by Evgeniy
02:14
created

ClosureExporter   A

Complexity

Total Complexity 39

Size/Duplication

Total Lines 170
Duplicated Lines 0 %

Test Coverage

Coverage 76.71%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 67
c 3
b 1
f 0
dl 0
loc 170
ccs 56
cts 73
cp 0.7671
rs 9.28
wmc 39

7 Methods

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