Completed
Push — master ( 34991a...adfc38 )
by Alexander
13s
created

ReflectionEngine::parseClassMethod()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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