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