Test Failed
Pull Request — master (#44)
by Evgeniy
02:33
created

ClosureExporter   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 116
Duplicated Lines 0 %

Test Coverage

Coverage 94.83%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 62
c 3
b 1
f 0
dl 0
loc 116
ccs 55
cts 58
cp 0.9483
rs 9.84
wmc 32

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A isCloseParenthesis() 0 3 1
A isOpenParenthesis() 0 3 1
D export() 0 76 26
A isUseNamespaceAlias() 0 10 3
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 6
 */
29
final class ClosureExporter
30 6
{
31 6
    private UseStatementParser $useStatementParser;
32
33
    public function __construct()
34
    {
35
        $this->useStatementParser = new UseStatementParser();
36
    }
37
38
    /**
39
     * Export closure as a string containing PHP code.
40
     *
41
     * @param Closure $closure Closure to export.
42 34
     *
43
     * @throws ReflectionException
44 34
     *
45
     * @return string String containing PHP code.
46 34
     */
47 34
    public function export(Closure $closure): string
48 34
    {
49
        $reflection = new ReflectionFunction($closure);
50 34
51
        $fileName = $reflection->getFileName();
52
        $start = $reflection->getStartLine();
53
        $end = $reflection->getEndLine();
54 34
55 34
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
56
            return 'function () {/* Error: unable to determine Closure source */}';
57 34
        }
58 34
59 34
        --$start;
60
        $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 34
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 20
                continue;
73
            }
74 20
            if ($closureTokens !== []) {
75 20
                $readableToken = $token[1] ?? $token;
76 20
                if (TokenHelper::isPartOfNamespace($token)) {
77
                    $buffer .= $token[1];
78
                    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 20
                        continue;
84
                    }
85 9
                    if (isset($uses[$buffer])) {
86 9
                        if ($this->isUseNamespaceAlias($buffer, $uses)) {
87
                            $previousUsePart = $uses[$buffer];
88
                            $buffer = '';
89 34
                            continue;
90 34
                        }
91 34
                        $readableToken = (empty($previousUsePart) || strpos($uses[$buffer], $previousUsePart) === false)
92 34
                            ? $previousUsePart . $uses[$buffer]
93 34
                            : $uses[$buffer]
94 7
                        ;
95
                        $buffer = '';
96 34
                        $previousUsePart = '';
97 31
                    } elseif (isset($uses[$token[1]])) {
98 29
                        $readableToken = $uses[$token[1]];
99 27
                        $previousUsePart = '';
100
                        $buffer = '';
101
                    }
102
                }
103 34
                if (is_string($token)) {
104
                    if ($this->isOpenParenthesis($token)) {
105
                        $pendingParenthesisCount++;
106
                    } elseif ($this->isCloseParenthesis($token)) {
107 34
                        if ($pendingParenthesisCount === 0) {
108
                            break;
109
                        }
110 34
                        $pendingParenthesisCount--;
111
                    } elseif ($token === ',' || $token === ';') {
112 34
                        if ($pendingParenthesisCount === 0) {
113
                            break;
114
                        }
115 34
                    }
116
                }
117 34
118
                $closureTokens[] = $readableToken;
119
            }
120
        }
121
122
        return implode('', $closureTokens);
123
    }
124
125 34
    private function isOpenParenthesis(string $value): bool
126
    {
127 34
        return in_array($value, ['{', '[', '(']);
128 34
    }
129
130
    private function isCloseParenthesis(string $value): bool
131 34
    {
132 34
        return in_array($value, ['}', ']', ')']);
133 34
    }
134 34
135 34
    private function isUseNamespaceAlias(string $useKey, array $uses): bool
136
    {
137
        if (!isset($uses[$useKey])) {
138
            return false;
139
        }
140
141
        $usesKeys = array_filter(explode('\\', (string) $uses[$useKey]));
142
        $lastPartUse = array_pop($usesKeys);
143
144
        return isset($uses[$lastPartUse]) && $uses[$lastPartUse] !== $uses[$useKey];
145
    }
146
}
147