1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Loaders;
13:
14: use Nette,
15: Nette\String,
16: Nette\Caching\Cache;
17:
18:
19:
20: 21: 22: 23: 24:
25: class RobotLoader extends AutoLoader
26: {
27:
28: public $scanDirs;
29:
30:
31: public $ignoreDirs = '.*, *.old, *.bak, *.tmp, temp';
32:
33:
34: public $acceptFiles = '*.php, *.php5';
35:
36:
37: public $autoRebuild = TRUE;
38:
39:
40: private $list = array();
41:
42:
43: private $files;
44:
45:
46: private $rebuilt = FALSE;
47:
48:
49: private $cacheStorage;
50:
51:
52:
53: 54:
55: public function __construct()
56: {
57: if (!extension_loaded('tokenizer')) {
58: throw new \Exception("PHP extension Tokenizer is not loaded.");
59: }
60: }
61:
62:
63:
64: 65: 66: 67:
68: public function register()
69: {
70: $cache = $this->getCache();
71: $key = $this->getKey();
72: if (isset($cache[$key])) {
73: $this->list = $cache[$key];
74: } else {
75: $this->rebuild();
76: }
77:
78: if (isset($this->list[strtolower(__CLASS__)]) && class_exists('Nette\Loaders\NetteLoader', FALSE)) {
79: NetteLoader::getInstance()->unregister();
80: }
81:
82: parent::register();
83: }
84:
85:
86:
87: 88: 89: 90: 91:
92: public function tryLoad($type)
93: {
94: $type = ltrim(strtolower($type), '\\'); 95:
96: if (isset($this->list[$type][0]) && !is_file($this->list[$type][0])) {
97: unset($this->list[$type]);
98: }
99:
100: if (!isset($this->list[$type])) {
101: $trace = debug_backtrace();
102: $initiator = & $trace[2]['function'];
103: if ($initiator === 'class_exists' || $initiator === 'interface_exists') {
104: $this->list[$type] = FALSE;
105: if ($this->autoRebuild && $this->rebuilt) {
106: $this->getCache()->save($this->getKey(), $this->list, array(
107: Cache::CONSTS => 'Nette\Framework::REVISION',
108: ));
109: }
110: }
111:
112: if ($this->autoRebuild && !$this->rebuilt) {
113: $this->rebuild();
114: }
115: }
116:
117: if (isset($this->list[$type][0])) {
118: LimitedScope::load($this->list[$type][0]);
119: self::$count++;
120: }
121: }
122:
123:
124:
125: 126: 127: 128:
129: public function rebuild()
130: {
131: $this->getCache()->save($this->getKey(), callback($this, '_rebuildCallback'), array(
132: Cache::CONSTS => 'Nette\Framework::REVISION',
133: ));
134: $this->rebuilt = TRUE;
135: }
136:
137:
138:
139: 140: 141:
142: public function _rebuildCallback()
143: {
144: foreach ($this->list as $pair) {
145: if ($pair) $this->files[$pair[0]] = $pair[1];
146: }
147: foreach (array_unique($this->scanDirs) as $dir) {
148: $this->scanDirectory($dir);
149: }
150: $this->files = NULL;
151: return $this->list;
152: }
153:
154:
155:
156: 157: 158:
159: public function getIndexedClasses()
160: {
161: $res = array();
162: foreach ($this->list as $class => $pair) {
163: if ($pair) $res[$pair[2]] = $pair[0];
164: }
165: return $res;
166: }
167:
168:
169:
170: 171: 172: 173: 174: 175:
176: public function addDirectory($path)
177: {
178: foreach ((array) $path as $val) {
179: $real = realpath($val);
180: if ($real === FALSE) {
181: throw new \DirectoryNotFoundException("Directory '$val' not found.");
182: }
183: $this->scanDirs[] = $real;
184: }
185: }
186:
187:
188:
189: 190: 191: 192: 193: 194: 195:
196: private function addClass($class, $file, $time)
197: {
198: $lClass = strtolower($class);
199: if (isset($this->list[$lClass][0]) && ($file2 = $this->list[$lClass][0]) !== $file && is_file($file2)) {
200: if ($this->files[$file2] !== filemtime($file2)) {
201: $this->scanScript($file2);
202: return $this->addClass($class, $file, $time);
203: }
204: $e = new \InvalidStateException("Ambiguous class '$class' resolution; defined in $file and in " . $this->list[$lClass][0] . ".");
205: {
206: throw $e;
207: }
208: }
209: $this->list[$lClass] = array($file, $time, $class);
210: $this->files[$file] = $time;
211: }
212:
213:
214:
215: 216: 217: 218: 219:
220: private function scanDirectory($dir)
221: {
222: if (is_dir($dir)) {
223: $disallow = array();
224: $iterator = Nette\Finder::findFiles(String::split($this->acceptFiles, '#[,\s]+#'))
225: ->filter(function($file) use (&$disallow){
226: return !isset($disallow[$file->getPathname()]);
227: })
228: ->from($dir)
229: ->exclude(String::split($this->ignoreDirs, '#[,\s]+#'))
230: ->filter($filter = function($dir) use (&$disallow){
231: $path = $dir->getPathname();
232: if (is_file("$path/netterobots.txt")) {
233: foreach (file("$path/netterobots.txt") as $s) {
234: if ($matches = String::match($s, '#^disallow\\s*:\\s*(\\S+)#i')) {
235: $disallow[$path . str_replace('/', DIRECTORY_SEPARATOR, rtrim('/' . ltrim($matches[1], '/'), '/'))] = TRUE;
236: }
237: }
238: }
239: return !isset($disallow[$path]);
240: });
241: $filter(new \SplFileInfo($dir));
242: } else {
243: $iterator = new \ArrayIterator(array(new \SplFileInfo($dir)));
244: }
245:
246: foreach ($iterator as $entry) {
247: $path = $entry->getPathname();
248: if (!isset($this->files[$path]) || $this->files[$path] !== $entry->getMTime()) {
249: $this->scanScript($path);
250: }
251: }
252: }
253:
254:
255:
256: 257: 258: 259: 260:
261: private function scanScript($file)
262: {
263: $T_NAMESPACE = PHP_VERSION_ID < 50300 ? -1 : T_NAMESPACE;
264: $T_NS_SEPARATOR = PHP_VERSION_ID < 50300 ? -1 : T_NS_SEPARATOR;
265:
266: $expected = FALSE;
267: $namespace = '';
268: $level = $minLevel = 0;
269: $time = filemtime($file);
270: $s = file_get_contents($file);
271:
272: foreach ($this->list as $class => $pair) {
273: if ($pair && $pair[0] === $file) unset($this->list[$class]);
274: }
275:
276: if ($matches = String::match($s, '#//nette'.'loader=(\S*)#')) {
277: foreach (explode(',', $matches[1]) as $name) {
278: $this->addClass($name, $file, $time);
279: }
280: return;
281: }
282:
283: foreach (token_get_all($s) 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: $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: if ($level === $minLevel) {
315: $this->addClass($namespace . $name, $file, $time);
316: }
317: break;
318:
319: case $T_NAMESPACE:
320: $namespace = $name ? $name . '\\' : '';
321: $minLevel = $token === '{' ? 1 : 0;
322: }
323:
324: $expected = NULL;
325: }
326:
327: if ($token === '{') {
328: $level++;
329: } elseif ($token === '}') {
330: $level--;
331: }
332: }
333: }
334:
335:
336:
337:
338:
339:
340:
341: 342: 343: 344:
345: public function setCacheStorage(Nette\Caching\ICacheStorage $storage)
346: {
347: $this->cacheStorage = $storage;
348: return $this;
349: }
350:
351:
352:
353: 354: 355:
356: public function getCacheStorage()
357: {
358: return $this->cacheStorage;
359: }
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\DummyStorage;
371: }
372: return new Cache($this->cacheStorage, 'Nette.RobotLoader');
373: }
374:
375:
376:
377: 378: 379:
380: protected function getKey()
381: {
382: return "v2|$this->ignoreDirs|$this->acceptFiles|" . implode('|', $this->scanDirs);
383: }
384:
385: }
386: