1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Reflection;
9:
10: use Nette;
11: use Nette\Utils\Strings;
12:
13:
14: 15: 16: 17:
18: class AnnotationsParser
19: {
20: use Nette\StaticClass;
21:
22:
23: const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';
24:
25:
26: const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF-\\\]*';
27:
28:
29: public static $useReflection;
30:
31:
32: public static $autoRefresh = true;
33:
34:
35: public static $inherited = ['description', 'param', 'return'];
36:
37:
38: private static $cache;
39:
40:
41: private static $timestamps;
42:
43:
44: private static $cacheStorage;
45:
46:
47: 48: 49: 50: 51:
52: public static function getAll(\Reflector $r)
53: {
54: if ($r instanceof \ReflectionClass) {
55: $type = $r->getName();
56: $member = 'class';
57: $file = $r->getFileName();
58:
59: } elseif ($r instanceof \ReflectionMethod) {
60: $type = $r->getDeclaringClass()->getName();
61: $member = $r->getName();
62: $file = $r->getFileName();
63:
64: } elseif ($r instanceof \ReflectionFunction) {
65: $type = null;
66: $member = $r->getName();
67: $file = $r->getFileName();
68:
69: } else {
70: $type = $r->getDeclaringClass()->getName();
71: $member = '$' . $r->getName();
72: $file = $r->getDeclaringClass()->getFileName();
73: }
74:
75: if (self::$useReflection === null) {
76: self::$useReflection = (bool) ClassType::from(__CLASS__)->getDocComment();
77: }
78:
79: if (!self::$useReflection) {
80: if ($file && isset(self::$timestamps[$file]) && self::$timestamps[$file] !== filemtime($file)) {
81: unset(self::$cache[$type]);
82: }
83: unset(self::$timestamps[$file]);
84: }
85:
86: if (isset(self::$cache[$type][$member])) {
87: return self::$cache[$type][$member];
88: }
89:
90: if (self::$useReflection) {
91: $annotations = self::parseComment($r->getDocComment());
92:
93: } else {
94: $outerCache = self::getCache();
95:
96: if (self::$cache === null) {
97: self::$cache = (array) $outerCache->load('list');
98: self::$timestamps = isset(self::$cache['*']) ? self::$cache['*'] : [];
99: }
100:
101: if (!isset(self::$cache[$type]) && $file) {
102: self::$cache['*'][$file] = filemtime($file);
103: foreach (static::parsePhp(file_get_contents($file)) as $class => $info) {
104: foreach ($info as $prop => $comment) {
105: if ($prop !== 'use') {
106: self::$cache[$class][$prop] = self::parseComment($comment);
107: }
108: }
109: }
110: $outerCache->save('list', self::$cache);
111: }
112:
113: if (isset(self::$cache[$type][$member])) {
114: $annotations = self::$cache[$type][$member];
115: } else {
116: $annotations = [];
117: }
118: }
119:
120: if ($r instanceof \ReflectionMethod && !$r->isPrivate()
121: && (!$r->isConstructor() || !empty($annotations['inheritdoc'][0]))
122: ) {
123: try {
124: $inherited = self::getAll(new \ReflectionMethod(get_parent_class($type), $member));
125: } catch (\ReflectionException $e) {
126: try {
127: $inherited = self::getAll($r->getPrototype());
128: } catch (\ReflectionException $e) {
129: $inherited = [];
130: }
131: }
132: $annotations += array_intersect_key($inherited, array_flip(self::$inherited));
133: }
134:
135: return self::$cache[$type][$member] = $annotations;
136: }
137:
138:
139: 140: 141: 142: 143: 144:
145: public static function expandClassName($name, \ReflectionClass $reflector)
146: {
147: if (empty($name)) {
148: throw new Nette\InvalidArgumentException('Class name must not be empty.');
149:
150: } elseif ($name === 'self') {
151: return $reflector->getName();
152:
153: } elseif ($name[0] === '\\') {
154: return ltrim($name, '\\');
155: }
156:
157: $filename = $reflector->getFileName();
158: $parsed = static::getCache()->load($filename, function (&$dp) use ($filename) {
159: if (self::$autoRefresh) {
160: $dp[Nette\Caching\Cache::FILES] = $filename;
161: }
162: return self::parsePhp(file_get_contents($filename));
163: });
164: $uses = array_change_key_case((array) $tmp = &$parsed[$reflector->getName()]['use']);
165: $parts = explode('\\', $name, 2);
166: $parts[0] = strtolower($parts[0]);
167: if (isset($uses[$parts[0]])) {
168: $parts[0] = $uses[$parts[0]];
169: return implode('\\', $parts);
170:
171: } elseif ($reflector->inNamespace()) {
172: return $reflector->getNamespaceName() . '\\' . $name;
173:
174: } else {
175: return $name;
176: }
177: }
178:
179:
180: 181: 182: 183: 184:
185: private static function ($comment)
186: {
187: static $tokens = ['true' => true, 'false' => false, 'null' => null, '' => true];
188:
189: $res = [];
190: $comment = preg_replace('#^\s*\*\s?#ms', '', trim($comment, '/*'));
191: $parts = preg_split('#^\s*(?=@' . self::RE_IDENTIFIER . ')#m', $comment, 2);
192:
193: $description = trim($parts[0]);
194: if ($description !== '') {
195: $res['description'] = [$description];
196: }
197:
198: $matches = Strings::matchAll(
199: isset($parts[1]) ? $parts[1] : '',
200: '~
201: (?<=\s|^)@(' . self::RE_IDENTIFIER . ')[ \t]* ## annotation
202: (
203: \((?>' . self::RE_STRING . '|[^\'")@]+)+\)| ## (value)
204: [^(@\r\n][^@\r\n]*|) ## value
205: ~xi'
206: );
207:
208: foreach ($matches as $match) {
209: list(, $name, $value) = $match;
210:
211: if (substr($value, 0, 1) === '(') {
212: $items = [];
213: $key = '';
214: $val = true;
215: $value[0] = ',';
216: while ($m = Strings::match(
217: $value,
218: '#\s*,\s*(?>(' . self::RE_IDENTIFIER . ')\s*=\s*)?(' . self::RE_STRING . '|[^\'"),\s][^\'"),]*)#A')
219: ) {
220: $value = substr($value, strlen($m[0]));
221: list(, $key, $val) = $m;
222: $val = rtrim($val);
223: if ($val[0] === "'" || $val[0] === '"') {
224: $val = substr($val, 1, -1);
225:
226: } elseif (is_numeric($val)) {
227: $val = 1 * $val;
228:
229: } else {
230: $lval = strtolower($val);
231: $val = array_key_exists($lval, $tokens) ? $tokens[$lval] : $val;
232: }
233:
234: if ($key === '') {
235: $items[] = $val;
236:
237: } else {
238: $items[$key] = $val;
239: }
240: }
241:
242: $value = count($items) < 2 && $key === '' ? $val : $items;
243:
244: } else {
245: $value = trim($value);
246: if (is_numeric($value)) {
247: $value = 1 * $value;
248:
249: } else {
250: $lval = strtolower($value);
251: $value = array_key_exists($lval, $tokens) ? $tokens[$lval] : $value;
252: }
253: }
254:
255: $res[$name][] = is_array($value) ? Nette\Utils\ArrayHash::from($value) : $value;
256: }
257:
258: return $res;
259: }
260:
261:
262: 263: 264: 265: 266: 267:
268: public static function parsePhp($code)
269: {
270: if (Strings::match($code, '#//nette' . 'loader=(\S*)#')) {
271: return;
272: }
273:
274: $tokens = @token_get_all($code);
275: $namespace = $class = $classLevel = $level = $docComment = null;
276: $res = $uses = [];
277:
278: while ($token = current($tokens)) {
279: next($tokens);
280: switch (is_array($token) ? $token[0] : $token) {
281: case T_DOC_COMMENT:
282: $docComment = $token[1];
283: break;
284:
285: case T_NAMESPACE:
286: $namespace = ltrim(self::fetch($tokens, [T_STRING, T_NS_SEPARATOR]) . '\\', '\\');
287: $uses = [];
288: break;
289:
290: case T_CLASS:
291: case T_INTERFACE:
292: case T_TRAIT:
293: if ($name = self::fetch($tokens, T_STRING)) {
294: $class = $namespace . $name;
295: $classLevel = $level + 1;
296: $res[$class] = [];
297: if ($docComment) {
298: $res[$class]['class'] = $docComment;
299: }
300: if ($uses) {
301: $res[$class]['use'] = $uses;
302: }
303: }
304: break;
305:
306: case T_FUNCTION:
307: self::fetch($tokens, '&');
308: if ($level === $classLevel && $docComment && ($name = self::fetch($tokens, T_STRING))) {
309: $res[$class][$name] = $docComment;
310: }
311: break;
312:
313: case T_VAR:
314: case T_PUBLIC:
315: case T_PROTECTED:
316: self::fetch($tokens, T_STATIC);
317: if ($level === $classLevel && $docComment && ($name = self::fetch($tokens, T_VARIABLE))) {
318: $res[$class][$name] = $docComment;
319: }
320: break;
321:
322: case T_USE:
323: while (!$class && ($name = self::fetch($tokens, [T_STRING, T_NS_SEPARATOR]))) {
324: $name = ltrim($name, '\\');
325: if (self::fetch($tokens, '{')) {
326: while ($suffix = self::fetch($tokens, [T_STRING, T_NS_SEPARATOR])) {
327: if (self::fetch($tokens, T_AS)) {
328: $uses[self::fetch($tokens, T_STRING)] = $name . $suffix;
329: } else {
330: $tmp = explode('\\', $suffix);
331: $uses[end($tmp)] = $name . $suffix;
332: }
333: if (!self::fetch($tokens, ',')) {
334: break;
335: }
336: }
337:
338: } elseif (self::fetch($tokens, T_AS)) {
339: $uses[self::fetch($tokens, T_STRING)] = $name;
340:
341: } else {
342: $tmp = explode('\\', $name);
343: $uses[end($tmp)] = $name;
344: }
345: if (!self::fetch($tokens, ',')) {
346: break;
347: }
348: }
349: break;
350:
351: case T_CURLY_OPEN:
352: case T_DOLLAR_OPEN_CURLY_BRACES:
353: case '{':
354: $level++;
355: break;
356:
357: case '}':
358: if ($level === $classLevel) {
359: $class = $classLevel = null;
360: }
361: $level--;
362:
363: case ';':
364: $docComment = null;
365: }
366: }
367:
368: return $res;
369: }
370:
371:
372: private static function fetch(&$tokens, $take)
373: {
374: $res = null;
375: while ($token = current($tokens)) {
376: list($token, $s) = is_array($token) ? $token : [$token, $token];
377: if (in_array($token, (array) $take, true)) {
378: $res .= $s;
379: } elseif (!in_array($token, [T_DOC_COMMENT, T_WHITESPACE, T_COMMENT], true)) {
380: break;
381: }
382: next($tokens);
383: }
384: return $res;
385: }
386:
387:
388:
389:
390:
391: 392: 393:
394: public static function setCacheStorage(Nette\Caching\IStorage $storage)
395: {
396: self::$cacheStorage = $storage;
397: }
398:
399:
400: 401: 402:
403: public static function getCacheStorage()
404: {
405: if (!self::$cacheStorage) {
406: self::$cacheStorage = new Nette\Caching\Storages\MemoryStorage();
407: }
408: return self::$cacheStorage;
409: }
410:
411:
412: 413: 414:
415: private static function getCache()
416: {
417: return new Nette\Caching\Cache(static::getCacheStorage(), 'Nette.Reflection.Annotations');
418: }
419: }
420: