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