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