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