Passed
Push — master ( 896efe...707faa )
by Alexander
02:16
created

ClosureExporter::isUseNamespaceAlias()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 5
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 10
ccs 5
cts 6
cp 0.8333
crap 3.0416
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 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 34
            if ($closureTokens !== []) {
75 34
                $readableToken = $token[1] ?? $token;
76 34
                if (TokenHelper::isPartOfNamespace($token)) {
77 20
                    $buffer .= $token[1];
78 20
                    if (PHP_VERSION_ID >= 80000 && $buffer !== '\\' && strpos($buffer, '\\') !== false) {
79
                        $usesKeys = array_filter(explode('\\', $buffer));
80
                        $buffer = array_pop($usesKeys);
81
                    }
82 20
                    if (!empty($previousUsePart) && $buffer === '\\') {
83 3
                        continue;
84
                    }
85 20
                    if (isset($uses[$buffer])) {
86 9
                        if ($this->isUseNamespaceAlias($buffer, $uses)) {
87 3
                            $previousUsePart = $uses[$buffer];
88 3
                            $buffer = '';
89 3
                            continue;
90
                        }
91 6
                        $readableToken = (empty($previousUsePart) || strpos($uses[$buffer], $previousUsePart) === false)
92 6
                            ? $previousUsePart . $uses[$buffer]
93 6
                            : $uses[$buffer]
94
                        ;
95 6
                        $buffer = '';
96 6
                        $previousUsePart = '';
97 20
                    } elseif (isset($uses[$token[1]])) {
98 3
                        $readableToken = $uses[$token[1]];
99 3
                        $previousUsePart = '';
100 3
                        $buffer = '';
101
                    }
102
                }
103 34
                if (is_string($token)) {
104 34
                    if ($this->isOpenParenthesis($token)) {
105 34
                        $pendingParenthesisCount++;
106 34
                    } elseif ($this->isCloseParenthesis($token)) {
107 34
                        if ($pendingParenthesisCount === 0) {
108 7
                            break;
109
                        }
110 34
                        $pendingParenthesisCount--;
111 31
                    } elseif ($token === ',' || $token === ';') {
112 29
                        if ($pendingParenthesisCount === 0) {
113 27
                            break;
114
                        }
115
                    }
116
                }
117
118 34
                $closureTokens[] = $readableToken;
119
            }
120
        }
121
122 34
        return implode('', $closureTokens);
123
    }
124
125 34
    private function isOpenParenthesis(string $value): bool
126
    {
127 34
        return in_array($value, ['{', '[', '(']);
128
    }
129
130 34
    private function isCloseParenthesis(string $value): bool
131
    {
132 34
        return in_array($value, ['}', ']', ')']);
133
    }
134
135 9
    private function isUseNamespaceAlias(string $useKey, array $uses): bool
136
    {
137 9
        if (!isset($uses[$useKey])) {
138
            return false;
139
        }
140
141 9
        $usesKeys = array_filter(explode('\\', (string) $uses[$useKey]));
142 9
        $lastPartUse = array_pop($usesKeys);
143
144 9
        return isset($uses[$lastPartUse]) && $uses[$lastPartUse] !== $uses[$useKey];
145
    }
146
}
147