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