1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20: 21: 22: 23:
24: class NRobotLoader extends NAutoLoader
25: {
26: const RETRY_LIMIT = 3;
27:
28:
29: public $scanDirs = array();
30:
31:
32: public $ignoreDirs = '.*, *.old, *.bak, *.tmp, temp';
33:
34:
35: public $acceptFiles = '*.php, *.php5';
36:
37:
38: public $autoRebuild = TRUE;
39:
40:
41: private $classes = array();
42:
43:
44: private $rebuilt = FALSE;
45:
46:
47: private $missing = array();
48:
49:
50: private $cacheStorage;
51:
52:
53:
54: public function __construct()
55: {
56: if (!extension_loaded('tokenizer')) {
57: throw new NotSupportedException("PHP extension Tokenizer is not loaded.");
58: }
59: }
60:
61:
62:
63: 64: 65: 66:
67: public function register()
68: {
69: $this->classes = $this->getCache()->load($this->getKey(), new NCallback($this, '_rebuildCallback'));
70: parent::register();
71: return $this;
72: }
73:
74:
75:
76: 77: 78: 79: 80:
81: public function tryLoad($type)
82: {
83: $type = ltrim(strtolower($type), '\\');
84:
85: $info = & $this->classes[$type];
86: if (isset($this->missing[$type]) || (is_int($info) && $info >= self::RETRY_LIMIT)) {
87: return;
88: }
89:
90: if ($this->autoRebuild) {
91: if (!is_array($info) || !is_file($info['file'])) {
92: $info = is_int($info) ? $info + 1 : 0;
93: if ($this->rebuilt) {
94: $this->getCache()->save($this->getKey(), $this->classes, array(
95: NCache::CONSTS => 'NFramework::REVISION',
96: ));
97: } else {
98: $this->rebuild();
99: }
100: } elseif (!$this->rebuilt && filemtime($info['file']) !== $info['time']) {
101: $this->updateFile($info['file']);
102: if (!isset($this->classes[$type])) {
103: $this->classes[$type] = 0;
104: }
105: $this->getCache()->save($this->getKey(), $this->classes, array(
106: NCache::CONSTS => 'NFramework::REVISION',
107: ));
108: }
109: }
110:
111: if (isset($this->classes[$type]['file'])) {
112: NLimitedScope::load($this->classes[$type]['file'], TRUE);
113: self::$count++;
114: } else {
115: $this->missing[$type] = TRUE;
116: }
117: }
118:
119:
120:
121: 122: 123: 124: 125: 126:
127: public function addDirectory($path)
128: {
129: foreach ((array) $path as $val) {
130: $real = realpath($val);
131: if ($real === FALSE) {
132: throw new DirectoryNotFoundException("Directory '$val' not found.");
133: }
134: $this->scanDirs[] = $real;
135: }
136: return $this;
137: }
138:
139:
140:
141: 142: 143:
144: public function getIndexedClasses()
145: {
146: $res = array();
147: foreach ($this->classes as $class => $info) {
148: if (is_array($info)) {
149: $res[$info['orig']] = $info['file'];
150: }
151: }
152: return $res;
153: }
154:
155:
156:
157: 158: 159: 160:
161: public function rebuild()
162: {
163: $this->rebuilt = TRUE;
164: $this->getCache()->save($this->getKey(), new NCallback($this, '_rebuildCallback'));
165: }
166:
167:
168:
169: 170: 171:
172: public function _rebuildCallback(& $dp)
173: {
174: $files = $missing = array();
175: foreach ($this->classes as $class => $info) {
176: if (is_array($info)) {
177: $files[$info['file']]['time'] = $info['time'];
178: $files[$info['file']]['classes'][] = $info['orig'];
179: } else {
180: $missing[$class] = $info;
181: }
182: }
183:
184: $this->classes = array();
185: foreach (array_unique($this->scanDirs) as $dir) {
186: foreach ($this->createFileIterator($dir) as $file) {
187: $file = $file->getPathname();
188: if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) {
189: $classes = $files[$file]['classes'];
190: } else {
191: $classes = $this->scanPhp(file_get_contents($file));
192: }
193:
194: foreach ($classes as $class) {
195: $info = & $this->classes[strtolower($class)];
196: if (isset($info['file'])) {
197: $e = new InvalidStateException("Ambiguous class $class resolution; defined in {$info['file']} and in $file.");
198: if (PHP_VERSION_ID < 50300) {
199: NDebugger::_exceptionHandler($e);
200: exit;
201: } else {
202: throw $e;
203: }
204: }
205: $info = array('file' => $file, 'time' => filemtime($file), 'orig' => $class);
206: }
207: }
208: }
209:
210: $dp = array(
211: NCache::CONSTS => 'NFramework::REVISION'
212: );
213: $this->classes += $missing;
214: return $this->classes;
215: }
216:
217:
218:
219: 220: 221: 222:
223: private function createFileIterator($dir)
224: {
225: if (!is_dir($dir)) {
226: return new ArrayIterator(array(new SplFileInfo($dir)));
227: }
228:
229: $ignoreDirs = is_array($this->ignoreDirs) ? $this->ignoreDirs : preg_split('#[,\s]+#', $this->ignoreDirs);
230: $disallow = array();
231: foreach ($ignoreDirs as $item) {
232: if ($item = realpath($item)) {
233: $disallow[$item] = TRUE;
234: }
235: }
236:
237: $iterator = NFinder::findFiles(is_array($this->acceptFiles) ? $this->acceptFiles : preg_split('#[,\s]+#', $this->acceptFiles))
238: ->filter(create_function('$file', 'extract($GLOBALS[0]['.array_push($GLOBALS[0], array('disallow'=>&$disallow)).'-1], EXTR_REFS);
239: return !isset($disallow[$file->getPathname()]);
240: '))
241: ->from($dir)
242: ->exclude($ignoreDirs)
243: ->filter($filter = create_function('$dir', 'extract($GLOBALS[0]['.array_push($GLOBALS[0], array('disallow'=>&$disallow)).'-1], EXTR_REFS);
244: $path = $dir->getPathname();
245: if (is_file("$path/netterobots.txt")) {
246: foreach (file("$path/netterobots.txt") as $s) {
247: if (preg_match(\'#^(?:disallow\\\\s*:)?\\\\s*(\\\\S+)#i\', $s, $matches)) {
248: $disallow[$path . str_replace(\'/\', DIRECTORY_SEPARATOR, rtrim(\'/\' . ltrim($matches[1], \'/\'), \'/\'))] = TRUE;
249: }
250: }
251: }
252: return !isset($disallow[$path]);
253: '));
254:
255: $filter(new SplFileInfo($dir));
256: return $iterator;
257: }
258:
259:
260:
261: 262: 263:
264: private function updateFile($file)
265: {
266: foreach ($this->classes as $class => $info) {
267: if (isset($info['file']) && $info['file'] === $file) {
268: unset($this->classes[$class]);
269: }
270: }
271:
272: if (is_file($file)) {
273: foreach ($this->scanPhp(file_get_contents($file)) as $class) {
274: $info = & $this->classes[strtolower($class)];
275: if (isset($info['file']) && @filemtime($info['file']) !== $info['time']) {
276: $this->updateFile($info['file']);
277: $info = & $this->classes[strtolower($class)];
278: }
279: if (isset($info['file'])) {
280: $e = new InvalidStateException("Ambiguous class $class resolution; defined in {$info['file']} and in $file.");
281: if (PHP_VERSION_ID < 50300) {
282: NDebugger::_exceptionHandler($e);
283: exit;
284: } else {
285: throw $e;
286: }
287: }
288: $info = array('file' => $file, 'time' => filemtime($file), 'orig' => $class);
289: }
290: }
291: }
292:
293:
294:
295: 296: 297: 298: 299:
300: private function scanPhp($code)
301: {
302: $T_NAMESPACE = PHP_VERSION_ID < 50300 ? -1 : T_NAMESPACE;
303: $T_NS_SEPARATOR = PHP_VERSION_ID < 50300 ? -1 : T_NS_SEPARATOR;
304: $T_TRAIT = PHP_VERSION_ID < 50400 ? -1 : T_TRAIT;
305:
306: $expected = FALSE;
307: $namespace = '';
308: $level = $minLevel = 0;
309: $classes = array();
310:
311: if (preg_match('#//nette'.'loader=(\S*)#', $code, $matches)) {
312: foreach (explode(',', $matches[1]) as $name) {
313: $classes[] = $name;
314: }
315: return $classes;
316: }
317:
318: foreach (@token_get_all($code) as $token) {
319: if (is_array($token)) {
320: switch ($token[0]) {
321: case T_COMMENT:
322: case T_DOC_COMMENT:
323: case T_WHITESPACE:
324: continue 2;
325:
326: case $T_NS_SEPARATOR:
327: case T_STRING:
328: if ($expected) {
329: $name .= $token[1];
330: }
331: continue 2;
332:
333: case $T_NAMESPACE:
334: case T_CLASS:
335: case T_INTERFACE:
336: case $T_TRAIT:
337: $expected = $token[0];
338: $name = '';
339: continue 2;
340: case T_CURLY_OPEN:
341: case T_DOLLAR_OPEN_CURLY_BRACES:
342: $level++;
343: }
344: }
345:
346: if ($expected) {
347: switch ($expected) {
348: case T_CLASS:
349: case T_INTERFACE:
350: case $T_TRAIT:
351: if ($level === $minLevel) {
352: $classes[] = $namespace . $name;
353: }
354: break;
355:
356: case $T_NAMESPACE:
357: $namespace = $name ? $name . '\\' : '';
358: $minLevel = $token === '{' ? 1 : 0;
359: }
360:
361: $expected = NULL;
362: }
363:
364: if ($token === '{') {
365: $level++;
366: } elseif ($token === '}') {
367: $level--;
368: }
369: }
370: return $classes;
371: }
372:
373:
374:
375:
376:
377:
378:
379: 380: 381:
382: public function setCacheStorage(ICacheStorage $storage)
383: {
384: $this->cacheStorage = $storage;
385: return $this;
386: }
387:
388:
389:
390: 391: 392:
393: public function getCacheStorage()
394: {
395: return $this->cacheStorage;
396: }
397:
398:
399:
400: 401: 402:
403: protected function getCache()
404: {
405: if (!$this->cacheStorage) {
406: trigger_error('Missing cache storage.', E_USER_WARNING);
407: $this->cacheStorage = new NDevNullStorage;
408: }
409: return new NCache($this->cacheStorage, 'Nette.RobotLoader');
410: }
411:
412:
413:
414: 415: 416:
417: protected function getKey()
418: {
419: return array($this->ignoreDirs, $this->acceptFiles, $this->scanDirs);
420: }
421:
422: }
423: