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