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