Passed
Pull Request — master (#34)
by Evgeniy
02:22
created

ClosureExporter   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 96
Duplicated Lines 0 %

Test Coverage

Coverage 94.34%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 50
c 2
b 1
f 0
dl 0
loc 96
ccs 50
cts 53
cp 0.9434
rs 10
wmc 30

3 Methods

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