1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Loaders;
9:
10: use Nette;
11: use Nette\Caching\Cache;
12: use SplFileInfo;
13:
14:
15: 16: 17:
18: class RobotLoader
19: {
20: use Nette\SmartObject;
21:
22: const RETRY_LIMIT = 3;
23:
24:
25: public $ignoreDirs = '.*, *.old, *.bak, *.tmp, temp';
26:
27:
28: public $acceptFiles = '*.php, *.php5';
29:
30:
31: public $autoRebuild = true;
32:
33:
34: private $scanPaths = [];
35:
36:
37: private $classes = [];
38:
39:
40: private $refreshed = false;
41:
42:
43: private $missing = [];
44:
45:
46: private $cacheStorage;
47:
48:
49: public function __construct()
50: {
51: if (!extension_loaded('tokenizer')) {
52: throw new Nette\NotSupportedException('PHP extension Tokenizer is not loaded.');
53: }
54: }
55:
56:
57: 58: 59: 60: 61:
62: public function register($prepend = false)
63: {
64: $this->classes = $this->getCache()->load($this->getKey(), [$this, 'rebuildCallback']);
65: spl_autoload_register([$this, 'tryLoad'], true, (bool) $prepend);
66: return $this;
67: }
68:
69:
70: 71: 72: 73: 74:
75: public function tryLoad($type)
76: {
77: $type = $orig = ltrim($type, '\\');
78: $type = 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->refreshed) {
89: $this->getCache()->save($this->getKey(), $this->classes);
90: } else {
91: $this->rebuild();
92: }
93: } elseif (!$this->refreshed && filemtime($info['file']) !== $info['time']) {
94: $this->updateFile($info['file']);
95: if (!isset($this->classes[$type])) {
96: $this->classes[$type] = 0;
97: }
98: $this->getCache()->save($this->getKey(), $this->classes);
99: }
100: }
101:
102: if (isset($this->classes[$type]['file'])) {
103: if ($this->classes[$type]['orig'] !== $orig) {
104: trigger_error("Case mismatch on class name '$orig', correct name is '{$this->classes[$type]['orig']}'.", E_USER_WARNING);
105: }
106: call_user_func(function ($file) { require $file; }, $this->classes[$type]['file']);
107: } else {
108: $this->missing[$type] = true;
109: }
110: }
111:
112:
113: 114: 115: 116: 117:
118: public function addDirectory($path)
119: {
120: $this->scanPaths = array_merge($this->scanPaths, (array) $path);
121: return $this;
122: }
123:
124:
125: 126: 127:
128: public function getIndexedClasses()
129: {
130: $res = [];
131: foreach ($this->classes as $info) {
132: if (is_array($info)) {
133: $res[$info['orig']] = $info['file'];
134: }
135: }
136: return $res;
137: }
138:
139:
140: 141: 142: 143:
144: public function rebuild()
145: {
146: if ($this->cacheStorage) {
147: $this->getCache()->save($this->getKey(), Nette\Utils\Callback::closure($this, 'rebuildCallback'));
148: } else {
149: $this->rebuildCallback();
150: }
151: }
152:
153:
154: 155: 156:
157: public function rebuildCallback()
158: {
159: $this->refreshed = true;
160: $files = $missing = [];
161: foreach ($this->classes as $class => $info) {
162: if (is_array($info)) {
163: $files[$info['file']]['time'] = $info['time'];
164: $files[$info['file']]['classes'][] = $info['orig'];
165: } else {
166: $missing[$class] = $info;
167: }
168: }
169:
170: $this->classes = [];
171: foreach ($this->scanPaths as $path) {
172: foreach (is_file($path) ? [new SplFileInfo($path)] : $this->createFileIterator($path) as $file) {
173: $file = $file->getPathname();
174: if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) {
175: $classes = $files[$file]['classes'];
176: } else {
177: $classes = $this->scanPhp(file_get_contents($file));
178: }
179: $files[$file] = ['classes' => [], 'time' => filemtime($file)];
180:
181: foreach ($classes as $class) {
182: $info = &$this->classes[strtolower($class)];
183: if (isset($info['file'])) {
184: throw new Nette\InvalidStateException("Ambiguous class $class resolution; defined in {$info['file']} and in $file.");
185: }
186: $info = ['file' => $file, 'time' => filemtime($file), 'orig' => $class];
187: }
188: }
189: }
190: $this->classes += $missing;
191: return $this->classes;
192: }
193:
194:
195: 196: 197: 198: 199:
200: private function createFileIterator($dir)
201: {
202: if (!is_dir($dir)) {
203: throw new Nette\IOException("File or directory '$dir' not found.");
204: }
205:
206: $ignoreDirs = is_array($this->ignoreDirs) ? $this->ignoreDirs : preg_split('#[,\s]+#', $this->ignoreDirs);
207: $disallow = [];
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 (SplFileInfo $file) use (&$disallow) {
216: return !isset($disallow[$file->getPathname()]);
217: })
218: ->from($dir)
219: ->exclude($ignoreDirs)
220: ->filter($filter = function (SplFileInfo $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 = ['file' => $file, 'time' => filemtime($file), 'orig' => $class];
259: }
260: }
261: }
262:
263:
264: 265: 266: 267: 268:
269: private function scanPhp($code)
270: {
271: $expected = false;
272: $namespace = '';
273: $level = $minLevel = 0;
274: $classes = [];
275:
276: if (preg_match('#//nette' . 'loader=(\S*)#', $code, $matches)) {
277: foreach (explode(',', $matches[1]) as $name) {
278: $classes[] = $name;
279: }
280: return $classes;
281: }
282:
283: foreach (@token_get_all($code) as $token) {
284: if (is_array($token)) {
285: switch ($token[0]) {
286: case T_COMMENT:
287: case T_DOC_COMMENT:
288: case T_WHITESPACE:
289: continue 2;
290:
291: case T_NS_SEPARATOR:
292: case T_STRING:
293: if ($expected) {
294: $name .= $token[1];
295: }
296: continue 2;
297:
298: case T_NAMESPACE:
299: case T_CLASS:
300: case T_INTERFACE:
301: case T_TRAIT:
302: $expected = $token[0];
303: $name = '';
304: continue 2;
305: case T_CURLY_OPEN:
306: case T_DOLLAR_OPEN_CURLY_BRACES:
307: $level++;
308: }
309: }
310:
311: if ($expected) {
312: switch ($expected) {
313: case T_CLASS:
314: case T_INTERFACE:
315: case T_TRAIT:
316: if ($name && $level === $minLevel) {
317: $classes[] = $namespace . $name;
318: }
319: break;
320:
321: case T_NAMESPACE:
322: $namespace = $name ? $name . '\\' : '';
323: $minLevel = $token === '{' ? 1 : 0;
324: }
325:
326: $expected = null;
327: }
328:
329: if ($token === '{') {
330: $level++;
331: } elseif ($token === '}') {
332: $level--;
333: }
334: }
335: return $classes;
336: }
337:
338:
339:
340:
341:
342: 343: 344: 345:
346: public function setAutoRefresh($on = true)
347: {
348: $this->autoRebuild = (bool) $on;
349: return $this;
350: }
351:
352:
353: 354: 355: 356:
357: public function setTempDirectory($dir)
358: {
359: if ($dir) {
360: Nette\Utils\FileSystem::createDir($dir);
361: $this->cacheStorage = new Nette\Caching\Storages\FileStorage($dir);
362: } else {
363: $this->cacheStorage = new Nette\Caching\Storages\DevNullStorage;
364: }
365: return $this;
366: }
367:
368:
369: 370: 371:
372: public function setCacheStorage(Nette\Caching\IStorage $storage)
373: {
374: $this->cacheStorage = $storage;
375: return $this;
376: }
377:
378:
379: 380: 381:
382: public function getCacheStorage()
383: {
384: return $this->cacheStorage;
385: }
386:
387:
388: 389: 390:
391: protected function getCache()
392: {
393: if (!$this->cacheStorage) {
394: trigger_error('Set path to temporary directory using setTempDirectory().', E_USER_WARNING);
395: $this->cacheStorage = new Nette\Caching\Storages\DevNullStorage;
396: }
397: return new Cache($this->cacheStorage, 'Nette.RobotLoader');
398: }
399:
400:
401: 402: 403:
404: protected function getKey()
405: {
406: return [$this->ignoreDirs, $this->acceptFiles, $this->scanPaths];
407: }
408: }
409: