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