Passed
Push — master ( b3e52d...ebba93 )
by Alexander
02:37 queued 19s
created

ClosureExporter::isOpenParenthesis()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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_key_exists;
12
use function array_slice;
13
use function defined;
14
use function in_array;
15
use function is_array;
16
use function is_string;
17
18
/**
19
 * ClosureExporter exports PHP {@see \Closure} as a string containing PHP code.
20
 *
21
 * The string is a valid PHP expression that can be evaluated by PHP parser
22
 * and the evaluation result will give back the closure instance.
23
 */
24
final class ClosureExporter
25
{
26
    private UseStatementParser $useStatementParser;
27
28 6
    public function __construct()
29
    {
30 6
        $this->useStatementParser = new UseStatementParser();
31 6
    }
32
33
    /**
34
     * Export closure as a string containing PHP code.
35
     *
36
     * @param Closure $closure Closure to export.
37
     *
38
     * @throws ReflectionException
39
     *
40
     * @return string String containing PHP code.
41
     */
42 34
    public function export(Closure $closure): string
43
    {
44 34
        $reflection = new ReflectionFunction($closure);
45
46 34
        $fileName = $reflection->getFileName();
47 34
        $start = $reflection->getStartLine();
48 34
        $end = $reflection->getEndLine();
49
50 34
        if ($fileName === false || $start === false || $end === false || ($fileContent = file($fileName)) === false) {
51
            return 'function() {/* Error: unable to determine Closure source */}';
52
        }
53
54 34
        --$start;
55 34
        $uses = $this->useStatementParser->fromFile($fileName);
56
57 34
        $source = implode('', array_slice($fileContent, $start, $end - $start));
58 34
        $tokens = token_get_all('<?php ' . $source);
59 34
        array_shift($tokens);
60
61 34
        $closureTokens = [];
62 34
        $pendingParenthesisCount = 0;
63 34
        $buffer = '';
64 34
        foreach ($tokens as $token) {
65 34
            if (in_array($token[0], [T_FUNCTION, T_FN, T_STATIC], true)) {
66 34
                $closureTokens[] = $token[1];
67 34
                continue;
68
            }
69 34
            if ($closureTokens !== []) {
70 34
                $readableToken = $token[1] ?? $token;
71 34
                if ($this->isNextTokenIsPartOfNamespace($token)) {
72 20
                    $buffer .= $token[1];
73
                    if (
74 20
                        PHP_VERSION_ID >= 80000
75 20
                        && $buffer !== '\\'
76 20
                        && strpos($buffer, '\\') !== false
77
                    ) {
78
                        $usesKeys = array_filter(explode('\\', $buffer));
79
                        $buffer = array_pop($usesKeys);
80
                    }
81
                    if (
82 20
                        array_key_exists($buffer, $uses)
83 20
                        && !$this->isNextTokenIsPartOfNamespace(next($tokens))
84
                    ) {
85 9
                        $readableToken = $uses[$buffer];
86 9
                        $buffer = '';
87
                    }
88
                }
89 34
                if (is_string($token)) {
90 34
                    if ($this->isOpenParenthesis($token)) {
91 34
                        $pendingParenthesisCount++;
92 34
                    } elseif ($this->isCloseParenthesis($token)) {
93 34
                        if ($pendingParenthesisCount === 0) {
94 7
                            break;
95
                        }
96 34
                        $pendingParenthesisCount--;
97 31
                    } elseif ($token === ',' || $token === ';') {
98 29
                        if ($pendingParenthesisCount === 0) {
99 27
                            break;
100
                        }
101
                    }
102
                }
103 34
                $closureTokens[] = $readableToken;
104
            }
105
        }
106
107 34
        return implode('', $closureTokens);
108
    }
109
110 34
    private function isOpenParenthesis(string $value): bool
111
    {
112 34
        return in_array($value, ['{', '[', '(']);
113
    }
114
115 34
    private function isCloseParenthesis(string $value): bool
116
    {
117 34
        return in_array($value, ['}', ']', ')']);
118
    }
119
120
    /**
121
     * @param array|string $token
122
     *
123
     * @return bool
124
     */
125 34
    private function isNextTokenIsPartOfNamespace($token): bool
126
    {
127 34
        if (!is_array($token)) {
128 34
            return false;
129
        }
130
131 34
        return $token[0] === T_STRING
132 34
            || $token[0] === T_NS_SEPARATOR
133 34
            || (defined('T_NAME_QUALIFIED') && $token[0] === T_NAME_QUALIFIED)
134 34
            || (defined('T_NAME_FULLY_QUALIFIED') && $token[0] === T_NAME_FULLY_QUALIFIED)
135 34
            || (defined('T_NAME_RELATIVE') && $token[0] === T_NAME_RELATIVE);
136
    }
137
}
138