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

ClosureExporter::export()   D

Complexity

Conditions 25
Paths 36

Size

Total Lines 71
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 25.3612

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 25
eloc 49
c 2
b 1
f 0
nc 36
nop 1
dl 0
loc 71
ccs 44
cts 48
cp 0.9167
crap 25.3612
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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