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