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