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