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