1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Loaders;
13:
14: use Nette,
15: Nette\Caching\Cache;
16:
17:
18: 19: 20: 21: 22: 23: 24: 25:
26: class RobotLoader extends AutoLoader
27: {
28: const RETRY_LIMIT = 3;
29:
30:
31: public $scanDirs = array();
32:
33:
34: public $ignoreDirs = '.*, *.old, *.bak, *.tmp, temp';
35:
36:
37: public $acceptFiles = '*.php, *.php5';
38:
39:
40: public $autoRebuild = TRUE;
41:
42:
43: private $classes = array();
44:
45:
46: private $rebuilt = FALSE;
47:
48:
49: private $missing = array();
50:
51:
52: private $cacheStorage;
53:
54:
55: public function __construct()
56: {
57: if (!extension_loaded('tokenizer')) {
58: throw new Nette\NotSupportedException("PHP extension Tokenizer is not loaded.");
59: }
60: }
61:
62:
63: 64: 65: 66:
67: public function register()
68: {
69: $this->classes = $this->getCache()->load($this->getKey(), new Nette\Callback($this, '_rebuildCallback'));
70: parent::register();
71: return $this;
72: }
73:
74:
75: 76: 77: 78: 79:
80: public function tryLoad($type)
81: {
82: $type = ltrim(strtolower($type), '\\');
83:
84: $info = & $this->classes[$type];
85: if (isset($this->missing[$type]) || (is_int($info) && $info >= self::RETRY_LIMIT)) {
86: return;
87: }
88:
89: if ($this->autoRebuild) {
90: if (!is_array($info) || !is_file($info['file'])) {
91: $info = is_int($info) ? $info + 1 : 0;
92: if ($this->rebuilt) {
93: $this->getCache()->save($this->getKey(), $this->classes, array(
94: Cache::CONSTS => 'Nette\Framework::REVISION',
95: ));
96: } else {
97: $this->rebuild();
98: }
99: } elseif (!$this->rebuilt && filemtime($info['file']) !== $info['time']) {
100: $this->updateFile($info['file']);
101: if (!isset($this->classes[$type])) {
102: $this->classes[$type] = 0;
103: }
104: $this->getCache()->save($this->getKey(), $this->classes, array(
105: Cache::CONSTS => 'Nette\Framework::REVISION',
106: ));
107: }
108: }
109:
110: if (isset($this->classes[$type]['file'])) {
111: Nette\Utils\LimitedScope::load($this->classes[$type]['file'], TRUE);
112: self::$count++;
113: } else {
114: $this->missing[$type] = TRUE;
115: }
116: }
117:
118:
119: 120: 121: 122: 123: 124:
125: public function addDirectory($path)
126: {
127: foreach ((array) $path as $val) {
128: $real = realpath($val);
129: if ($real === FALSE) {
130: throw new Nette\DirectoryNotFoundException("Directory '$val' not found.");
131: }
132: $this->scanDirs[] = $real;
133: }
134: return $this;
135: }
136:
137:
138: 139: 140:
141: public function getIndexedClasses()
142: {
143: $res = array();
144: foreach ($this->classes as $class => $info) {
145: if (is_array($info)) {
146: $res[$info['orig']] = $info['file'];
147: }
148: }
149: return $res;
150: }
151:
152:
153: 154: 155: 156:
157: public function rebuild()
158: {
159: $this->rebuilt = TRUE;
160: $this->getCache()->save($this->getKey(), new Nette\Callback($this, '_rebuildCallback'));
161: }
162:
163:
164: 165: 166:
167: public function _rebuildCallback(& $dp)
168: {
169: $files = $missing = array();
170: foreach ($this->classes as $class => $info) {
171: if (is_array($info)) {
172: $files[$info['file']]['time'] = $info['time'];
173: $files[$info['file']]['classes'][] = $info['orig'];
174: } else {
175: $missing[$class] = $info;
176: }
177: }
178:
179: $this->classes = array();
180: foreach (array_unique($this->scanDirs) as $dir) {
181: foreach ($this->createFileIterator($dir) as $file) {
182: $file = $file->getPathname();
183: if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) {
184: $classes = $files[$file]['classes'];
185: } else {
186: $classes = $this->scanPhp(file_get_contents($file));
187: }
188:
189: foreach ($classes as $class) {
190: $info = & $this->classes[strtolower($class)];
191: if (isset($info['file'])) {
192: $e = new Nette\InvalidStateException("Ambiguous class $class resolution; defined in {$info['file']} and in $file.");
193: {
194: throw $e;
195: }
196: }
197: $info = array('file' => $file, 'time' => filemtime($file), 'orig' => $class);
198: }
199: }
200: }
201:
202: $dp = array(
203: Cache::CONSTS => 'Nette\Framework::REVISION'
204: );
205: $this->classes += $missing;
206: return $this->classes;
207: }
208:
209:
210: 211: 212: 213:
214: private function createFileIterator($dir)
215: {
216: if (!is_dir($dir)) {
217: return new \ArrayIterator(array(new \SplFileInfo($dir)));
218: }
219:
220: $ignoreDirs = is_array($this->ignoreDirs) ? $this->ignoreDirs : preg_split('#[,\s]+#', $this->ignoreDirs);
221: $disallow = array();
222: foreach ($ignoreDirs as $item) {
223: if ($item = realpath($item)) {
224: $disallow[$item] = TRUE;
225: }
226: }
227:
228: $iterator = Nette\Utils\Finder::findFiles(is_array($this->acceptFiles) ? $this->acceptFiles : preg_split('#[,\s]+#', $this->acceptFiles))
229: ->filter(function($file) use (& $disallow){
230: return !isset($disallow[$file->getPathname()]);
231: })
232: ->from($dir)
233: ->exclude($ignoreDirs)
234: ->filter($filter = function($dir) use (& $disallow){
235: $path = $dir->getPathname();
236: if (is_file("$path/netterobots.txt")) {
237: foreach (file("$path/netterobots.txt") as $s) {
238: if (preg_match('#^(?:disallow\\s*:)?\\s*(\\S+)#i', $s, $matches)) {
239: $disallow[$path . str_replace('/', DIRECTORY_SEPARATOR, rtrim('/' . ltrim($matches[1], '/'), '/'))] = TRUE;
240: }
241: }
242: }
243: return !isset($disallow[$path]);
244: });
245:
246: $filter(new \SplFileInfo($dir));
247: return $iterator;
248: }
249:
250:
251: 252: 253:
254: private function updateFile($file)
255: {
256: foreach ($this->classes as $class => $info) {
257: if (isset($info['file']) && $info['file'] === $file) {
258: unset($this->classes[$class]);
259: }
260: }
261:
262: if (is_file($file)) {
263: foreach ($this->scanPhp(file_get_contents($file)) as $class) {
264: $info = & $this->classes[strtolower($class)];
265: if (isset($info['file']) && @filemtime($info['file']) !== $info['time']) {
266: $this->updateFile($info['file']);
267: $info = & $this->classes[strtolower($class)];
268: }
269: if (isset($info['file'])) {
270: $e = new Nette\InvalidStateException("Ambiguous class $class resolution; defined in {$info['file']} and in $file.");
271: {
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 ($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(Nette\Caching\IStorage $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 Nette\Caching\Storages\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: