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

ClosureExporter::isUseConsistingOfMultipleParts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
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 5
    public function __construct()
34
    {
35 5
        $this->useStatementParser = new UseStatementParser();
36 5
    }
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 48
    public function export(Closure $closure): string
48
    {
49 48
        $reflection = new ReflectionFunction($closure);
50
51 48
        $fileName = $reflection->getFileName();
52 48
        $start = $reflection->getStartLine();
53 48
        $end = $reflection->getEndLine();
54
55 48
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
56
            return 'function () {/* Error: unable to determine Closure source */}';
57
        }
58
59 48
        --$start;
60 48
        $uses = $this->useStatementParser->fromFile($fileName);
61 48
        $tokens = token_get_all('<?php ' . implode('', array_slice($fileContent, $start, $end - $start)));
62 48
        array_shift($tokens);
63
64 48
        $buffer = '';
65 48
        $closureTokens = [];
66 48
        $previousUsePart = '';
67 48
        $pendingParenthesisCount = 0;
68
69 48
        foreach ($tokens as $token) {
70 48
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
71 48
                $closureTokens[] = $token[1];
72 48
                continue;
73
            }
74
75 48
            if ($closureTokens === []) {
76 48
                continue;
77
            }
78
79 48
            $readableToken = is_array($token) ? $token[1] : $token;
80
81 48
            if ($this->useStatementParser->isTokenIsPartOfUse($token)) {
82 29
                $buffer .= $token[1];
83 29
                if ($this->isUseConsistingOfMultipleParts($buffer)) {
84 4
                    $buffer = $this->getUseLastPart($buffer);
85
                }
86 29
                if (!empty($previousUsePart) && $buffer === '\\') {
87 4
                    continue;
88
                }
89 29
                if (isset($uses[$buffer])) {
90 17
                    if ($this->isNotFullUseAlias($uses[$buffer], $uses)) {
91 4
                        $previousUsePart = $uses[$buffer];
92 4
                        $buffer = '';
93 4
                        continue;
94
                    }
95 17
                    $readableToken = (empty($previousUsePart) || strpos($uses[$buffer], $previousUsePart) === false)
96 17
                        ? $previousUsePart . $uses[$buffer]
97 17
                        : $uses[$buffer]
98
                    ;
99 17
                    $buffer = '';
100 17
                    $previousUsePart = '';
101 13
                } elseif (isset($uses[$token[1]])) {
102 1
                    $readableToken = $uses[$token[1]];
103 1
                    $previousUsePart = '';
104 1
                    $buffer = '';
105
                }
106
            }
107
108 48
            if (is_string($token)) {
109 48
                if ($this->isOpenParenthesis($token)) {
110 48
                    $pendingParenthesisCount++;
111 48
                } elseif ($this->isCloseParenthesis($token)) {
112 48
                    if ($pendingParenthesisCount === 0) {
113 9
                        break;
114
                    }
115 48
                    $pendingParenthesisCount--;
116 44
                } elseif ($token === ',' || $token === ';') {
117 41
                    if ($pendingParenthesisCount === 0) {
118 39
                        break;
119
                    }
120
                }
121
            }
122
123 48
            $closureTokens[] = $readableToken;
124
        }
125
126 48
        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 17
    private function getUseLastPart(string $use): string
137
    {
138 17
        $parts = array_filter(explode('\\', $use));
139 17
        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 29
    private function isUseConsistingOfMultipleParts(string $use): bool
150
    {
151 29
        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 $use The use statement data to check.
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 17
    private function isNotFullUseAlias(string $use, array $uses): bool
163
    {
164 17
        $lastPart = $this->getUseLastPart($use);
165 17
        return isset($uses[$lastPart]) && $uses[$lastPart] !== $use;
166
    }
167
168
    /**
169
     * Checks whether the value of the token is an opening parenthesis.
170
     *
171
     * @param string $value The token value.
172
     *
173
     * @return bool Whether the value of the token is an opening parenthesis.
174
     */
175 48
    private function isOpenParenthesis(string $value): bool
176
    {
177 48
        return in_array($value, ['{', '[', '(']);
178
    }
179
180
    /**
181
     * Checks whether the value of the token is a closing parenthesis.
182
     *
183
     * @param string $value The token value.
184
     *
185
     * @return bool Whether the value of the token is a closing parenthesis.
186
     */
187 48
    private function isCloseParenthesis(string $value): bool
188
    {
189 48
        return in_array($value, ['}', ']', ')']);
190
    }
191
}
192