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