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