Completed
Push — master ( 793727...6af734 )
by Alexander
02:36
created

ReflectionEngine::parseClassConstant()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 0
cts 9
cp 0
rs 9.3888
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 30
1
<?php
2
/**
3
 * Parser Reflection API
4
 *
5
 * @copyright Copyright 2015, Lisachenko Alexander <[email protected]>
6
 *
7
 * This source file is subject to the license that is bundled
8
 * with this source code in the file LICENSE.
9
 */
10
11
namespace Go\ParserReflection;
12
13
use Go\ParserReflection\Instrument\PathResolver;
14
use Go\ParserReflection\NodeVisitor\RootNamespaceNormalizer;
15
use PhpParser\Lexer;
16
use PhpParser\Node;
17
use PhpParser\Node\Stmt\ClassConst;
18
use PhpParser\Node\Stmt\ClassLike;
19
use PhpParser\Node\Stmt\ClassMethod;
20
use PhpParser\Node\Stmt\Namespace_;
21
use PhpParser\Node\Stmt\Property;
22
use PhpParser\NodeTraverser;
23
use PhpParser\NodeVisitor\NameResolver;
24
use PhpParser\Parser;
25
use PhpParser\ParserFactory;
26
27
/**
28
 * AST-based reflection engine, powered by PHP-Parser
29
 */
30
class ReflectionEngine
31
{
32
    /**
33
     * @var null|LocatorInterface
34
     */
35
    protected static $locator = null;
36
37
    /**
38
     * @var array|Node[]
39
     */
40
    protected static $parsedFiles = array();
41
42
    /**
43
     * @var null|integer
44
     */
45
    protected static $maximumCachedFiles;
46
47
    /**
48
     * @var null|Parser
49
     */
50
    protected static $parser = null;
51
52
    /**
53
     * @var null|NodeTraverser
54
     */
55
    protected static $traverser = null;
56
57
    /**
58
     * @var null|Lexer
59
     */
60
    protected static $lexer = null;
61
62
    private function __construct() {}
63
64 1
    public static function init(LocatorInterface $locator)
65
    {
66 1
        self::$lexer = new Lexer(['usedAttributes' => [
67
            'comments',
68
            'startLine',
69
            'endLine',
70
            'startTokenPos',
71
            'endTokenPos',
72
            'startFilePos',
73
            'endFilePos'
74
        ]]);
75
76 1
        self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, self::$lexer);
77
78 1
        self::$traverser = $traverser = new NodeTraverser();
79 1
        $traverser->addVisitor(new NameResolver());
80 1
        $traverser->addVisitor(new RootNamespaceNormalizer());
81
82 1
        self::$locator = $locator;
83 1
    }
84
85
    /**
86
     * Limits number of files, that can be cached at any given moment
87
     *
88
     * @param integer $newLimit New limit
89
     *
90
     * @return void
91
     */
92
    public static function setMaximumCachedFiles($newLimit)
93
    {
94
        self::$maximumCachedFiles = $newLimit;
95
        if (count(self::$parsedFiles) > $newLimit) {
96
            self::$parsedFiles = array_slice(self::$parsedFiles, 0, $newLimit);
97
        }
98
    }
99
100
    /**
101
     * Locates a file name for class
102
     *
103
     * @param string $fullClassName Full name of the class
104
     *
105
     * @return string
106
     */
107 22
    public static function locateClassFile($fullClassName)
108
    {
109 22
        if (class_exists($fullClassName, false)
110 3
            || interface_exists($fullClassName, false)
111 22
            || trait_exists($fullClassName, false)
112
        ) {
113 21
            $refClass      = new \ReflectionClass($fullClassName);
114 21
            $classFileName = $refClass->getFileName();
115
        } else {
116 1
            $classFileName = self::$locator->locateClass($fullClassName);
117
        }
118
119 22
        if (!$classFileName) {
120
            throw new \InvalidArgumentException("Class $fullClassName was not found by locator");
121
        }
122
123 22
        return $classFileName;
124
    }
125
126
    /**
127
     * Tries to parse a class by name using LocatorInterface
128
     *
129
     * @param string $fullClassName Class name to load
130
     *
131
     * @return ClassLike
132
     */
133 22
    public static function parseClass($fullClassName)
134
    {
135 22
        $classFileName  = self::locateClassFile($fullClassName);
136 22
        $namespaceParts = explode('\\', $fullClassName);
137 22
        $className      = array_pop($namespaceParts);
138 22
        $namespaceName  = join('\\', $namespaceParts);
139
140
        // we have a namespace node somewhere
141 22
        $namespace      = self::parseFileNamespace($classFileName, $namespaceName);
142 22
        $namespaceNodes = $namespace->stmts;
143
144 22
        foreach ($namespaceNodes as $namespaceLevelNode) {
145 22
            if ($namespaceLevelNode instanceof ClassLike && $namespaceLevelNode->name == $className) {
146 22
                $namespaceLevelNode->setAttribute('fileName', $classFileName);
147
148 22
                return $namespaceLevelNode;
149
            }
150
        }
151
152
        throw new \InvalidArgumentException("Class $fullClassName was not found in the $classFileName");
153
    }
154
155
    /**
156
     * Parses class method
157
     *
158
     * @param string $fullClassName Name of the class
159
     * @param string $methodName Name of the method
160
     *
161
     * @return ClassMethod
162
     */
163
    public static function parseClassMethod($fullClassName, $methodName)
164
    {
165
        $class      = self::parseClass($fullClassName);
166
        $classNodes = $class->stmts;
167
168
        foreach ($classNodes as $classLevelNode) {
169
            if ($classLevelNode instanceof ClassMethod && $classLevelNode->name->toString() == $methodName) {
170
                return $classLevelNode;
171
            }
172
        }
173
174
        throw new \InvalidArgumentException("Method $methodName was not found in the $fullClassName");
175
    }
176
177
    /**
178
     * Parses class property
179
     *
180
     * @param string $fullClassName Name of the class
181
     * @param string $propertyName Name of the property
182
     *
183
     * @return array Pair of [Property and PropertyProperty] nodes
184
     */
185 1
    public static function parseClassProperty($fullClassName, $propertyName)
186
    {
187 1
        $class      = self::parseClass($fullClassName);
188 1
        $classNodes = $class->stmts;
189
190 1
        foreach ($classNodes as $classLevelNode) {
191 1
            if ($classLevelNode instanceof Property) {
192 1
                foreach ($classLevelNode->props as $classProperty) {
193 1
                    if ($classProperty->name->toString() == $propertyName) {
194 1
                        return [$classLevelNode, $classProperty];
195
                    }
196
                }
197
            }
198
        }
199
200
        throw new \InvalidArgumentException("Property $propertyName was not found in the $fullClassName");
201
    }
202
203
    /**
204
     * Parses class constants
205
     *
206
     * @param string $fullClassName
207
     * @param string $constantName
208
     * @return array Pair of [ClassConst and Const_] nodes
209
     */
210
    public static function parseClassConstant(string $fullClassName, string $constantName): array
211
    {
212
        $class      = self::parseClass($fullClassName);
213
        $classNodes = $class->stmts;
214
215
        foreach ($classNodes as $classLevelNode) {
216
            if ($classLevelNode instanceof ClassConst) {
217
                foreach ($classLevelNode->consts as $classConst) {
218
                    if ($classConst->name->toString() === $constantName) {
219
                        return [$classLevelNode, $classConst];
220
                    }
221
                }
222
            }
223
        }
224
225
        throw new \InvalidArgumentException("ClassConstant $constantName was not found in the $fullClassName");
226
    }
227
228
    /**
229
     * Parses a file and returns an AST for it
230
     *
231
     * @param string      $fileName Name of the file
232
     * @param string|null $fileContent Optional content of the file
233
     *
234
     * @return \PhpParser\Node[]
235
     */
236 3056
    public static function parseFile($fileName, $fileContent = null)
237
    {
238 3056
        $fileName = PathResolver::realpath($fileName);
239 3056
        if (isset(self::$parsedFiles[$fileName]) && !isset($fileContent)) {
240 3054
            return self::$parsedFiles[$fileName];
241
        }
242
243 9
        if (isset(self::$maximumCachedFiles) && (count(self::$parsedFiles) === self::$maximumCachedFiles)) {
244
            array_shift(self::$parsedFiles);
245
        }
246
247 9
        if (!isset($fileContent)) {
248 9
            $fileContent = file_get_contents($fileName);
249
        }
250 9
        $treeNode = self::$parser->parse($fileContent);
251 9
        $treeNode = self::$traverser->traverse($treeNode);
252
253 9
        self::$parsedFiles[$fileName] = $treeNode;
254
255 9
        return $treeNode;
256
    }
257
258
    /**
259
     * Parses a file namespace and returns an AST for it
260
     *
261
     * @param string $fileName Name of the file
262
     * @param string $namespaceName Namespace name
263
     *
264
     * @return Namespace_
265
     * @throws ReflectionException
266
     */
267 37
    public static function parseFileNamespace($fileName, $namespaceName)
268
    {
269 37
        $topLevelNodes = self::parseFile($fileName);
270
        // namespaces can be only top-level nodes, so we can scan them directly
271 37
        foreach ($topLevelNodes as $topLevelNode) {
272 37
            if (!$topLevelNode instanceof Namespace_) {
273 2
                continue;
274
            }
275 37
            $topLevelNodeName = $topLevelNode->name ? $topLevelNode->name->toString() : '';
276 37
            if (ltrim($topLevelNodeName, '\\') === trim($namespaceName, '\\')) {
277 37
                return $topLevelNode;
278
            }
279
        }
280
281
        throw new ReflectionException("Namespace $namespaceName was not found in the file $fileName");
282
    }
283
284
}
285