Passed
Pull Request — master (#34)
by Evgeniy
01:51
created

ClosureExporter   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 102
Duplicated Lines 0 %

Test Coverage

Coverage 93.22%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 58
c 2
b 1
f 0
dl 0
loc 102
ccs 55
cts 59
cp 0.9322
rs 9.6
wmc 35

3 Methods

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