1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10:
11:
12: namespace Nette\Loaders;
13:
14: use Nette;
15:
16:
17:
18: 19: 20: 21: 22:
23: class RobotLoader extends AutoLoader
24: {
25:
26: public $scanDirs;
27:
28:
29: public $ignoreDirs = '.*, *.old, *.bak, *.tmp, temp';
30:
31:
32: public $acceptFiles = '*.php, *.php5';
33:
34:
35: public $autoRebuild;
36:
37:
38: private $list = array();
39:
40:
41: private $files;
42:
43:
44: private $rebuilt = FALSE;
45:
46:
47: private $acceptMask;
48:
49:
50: private $ignoreMask;
51:
52:
53:
54: 55:
56: public function __construct()
57: {
58: if (!extension_loaded('tokenizer')) {
59: throw new \Exception("PHP extension Tokenizer is not loaded.");
60: }
61: }
62:
63:
64:
65: 66: 67: 68:
69: public function register()
70: {
71: $cache = $this->getCache();
72: $key = $this->getKey();
73: if (isset($cache[$key])) {
74: $this->list = $cache[$key];
75: } else {
76: $this->rebuild();
77: }
78:
79: if (isset($this->list[strtolower(__CLASS__)]) && class_exists('Nette\Loaders\NetteLoader', FALSE)) {
80: NetteLoader::getInstance()->unregister();
81: }
82:
83: parent::register();
84: }
85:
86:
87:
88: 89: 90: 91: 92:
93: public function tryLoad($type)
94: {
95: $type = ltrim(strtolower($type), '\\');
96: if (isset($this->list[$type])) {
97: if ($this->list[$type] !== FALSE) {
98: LimitedScope::load($this->list[$type][0]);
99: self::$count++;
100: }
101:
102: } else {
103: $this->list[$type] = FALSE;
104:
105: if ($this->autoRebuild === NULL) {
106: $this->autoRebuild = !$this->isProduction();
107: }
108:
109: if ($this->autoRebuild) {
110: if ($this->rebuilt) {
111: $this->getCache()->save($this->getKey(), $this->list);
112: } else {
113: $this->rebuild();
114: }
115: }
116:
117: if ($this->list[$type] !== FALSE) {
118: LimitedScope::load($this->list[$type][0], TRUE);
119: self::$count++;
120: }
121: }
122: }
123:
124:
125:
126: 127: 128: 129:
130: public function rebuild()
131: {
132: $this->getCache()->save($this->getKey(), callback($this, '_rebuildCallback'));
133: $this->rebuilt = TRUE;
134: }
135:
136:
137:
138: 139: 140:
141: public function _rebuildCallback()
142: {
143: $this->acceptMask = self::wildcards2re($this->acceptFiles);
144: $this->ignoreMask = self::wildcards2re($this->ignoreDirs);
145: foreach ($this->list as $pair) {
146: if ($pair) $this->files[$pair[0]] = $pair[1];
147: }
148: foreach (array_unique($this->scanDirs) as $dir) {
149: $this->scanDirectory($dir);
150: }
151: $this->files = NULL;
152: return $this->list;
153: }
154:
155:
156:
157: 158: 159:
160: public function getIndexedClasses()
161: {
162: $res = array();
163: foreach ($this->list as $class => $pair) {
164: if ($pair) $res[$class] = $pair[0];
165: }
166: return $res;
167: }
168:
169:
170:
171: 172: 173: 174: 175: 176:
177: public function addDirectory($path)
178: {
179: foreach ((array) $path as $val) {
180: $real = realpath($val);
181: if ($real === FALSE) {
182: throw new \DirectoryNotFoundException("Directory '$val' not found.");
183: }
184: $this->scanDirs[] = $real;
185: }
186: }
187:
188:
189:
190: 191: 192: 193: 194: 195: 196:
197: private function addClass($class, $file, $time)
198: {
199: $class = strtolower($class);
200: if (!empty($this->list[$class]) && $this->list[$class][0] !== $file) {
201: spl_autoload_call($class);
202: throw new \InvalidStateException("Ambiguous class '$class' resolution; defined in $file and in " . $this->list[$class][0] . ".");
203: }
204: $this->list[$class] = array($file, $time);
205: }
206:
207:
208:
209: 210: 211: 212: 213:
214: private function scanDirectory($dir)
215: {
216: if (is_file($dir)) {
217: if (!isset($this->files[$dir]) || $this->files[$dir] !== filemtime($dir)) {
218: $this->scanScript($dir);
219: }
220: return;
221: }
222:
223: $iterator = dir($dir);
224: if (!$iterator) return;
225:
226: $disallow = array();
227: if (is_file($dir . '/netterobots.txt')) {
228: foreach (file($dir . '/netterobots.txt') as $s) {
229: if (preg_match('#^disallow\\s*:\\s*(\\S+)#i', $s, $m)) {
230: $disallow[trim($m[1], '/')] = TRUE;
231: }
232: }
233: if (isset($disallow[''])) return;
234: }
235:
236: while (FALSE !== ($entry = $iterator->read())) {
237: if ($entry == '.' || $entry == '..' || isset($disallow[$entry])) continue;
238:
239: $path = $dir . DIRECTORY_SEPARATOR . $entry;
240:
241:
242: if (is_dir($path)) {
243:
244: if (!preg_match($this->ignoreMask, $entry)) {
245: $this->scanDirectory($path);
246: }
247: continue;
248: }
249:
250: if (is_file($path) && preg_match($this->acceptMask, $entry)) {
251: if (!isset($this->files[$path]) || $this->files[$path] !== filemtime($path)) {
252: $this->scanScript($path);
253: }
254: }
255: }
256:
257: $iterator->close();
258: }
259:
260:
261:
262: 263: 264: 265: 266:
267: private function scanScript($file)
268: {
269: $T_NAMESPACE = PHP_VERSION_ID < 50300 ? -1 : T_NAMESPACE;
270: $T_NS_SEPARATOR = PHP_VERSION_ID < 50300 ? -1 : T_NS_SEPARATOR;
271:
272: $expected = FALSE;
273: $namespace = '';
274: $level = $minLevel = 0;
275: $time = filemtime($file);
276: $s = file_get_contents($file);
277:
278: if (preg_match('#//nette'.'loader=(\S*)#', $s, $matches)) {
279: foreach (explode(',', $matches[1]) as $name) {
280: $this->addClass($name, $file, $time);
281: }
282: return;
283: }
284:
285: foreach (token_get_all($s) as $token)
286: {
287: if (is_array($token)) {
288: switch ($token[0]) {
289: case T_COMMENT:
290: case T_DOC_COMMENT:
291: case T_WHITESPACE:
292: continue 2;
293:
294: case $T_NS_SEPARATOR:
295: case T_STRING:
296: if ($expected) {
297: $name .= $token[1];
298: }
299: continue 2;
300:
301: case $T_NAMESPACE:
302: case T_CLASS:
303: case T_INTERFACE:
304: $expected = $token[0];
305: $name = '';
306: continue 2;
307: case T_CURLY_OPEN:
308: case T_DOLLAR_OPEN_CURLY_BRACES:
309: $level++;
310: }
311: }
312:
313: if ($expected) {
314: switch ($expected) {
315: case T_CLASS:
316: case T_INTERFACE:
317: if ($level === $minLevel) {
318: $this->addClass($namespace . $name, $file, $time);
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: }
337:
338:
339:
340: 341: 342: 343: 344:
345: private static function wildcards2re($wildcards)
346: {
347: $mask = array();
348: foreach (explode(',', $wildcards) as $wildcard) {
349: $wildcard = trim($wildcard);
350: $wildcard = addcslashes($wildcard, '.\\+[^]$(){}=!><|:#');
351: $wildcard = strtr($wildcard, array('*' => '.*', '?' => '.'));
352: $mask[] = $wildcard;
353: }
354: return '#^(' . implode('|', $mask) . ')$#i';
355: }
356:
357:
358:
359:
360:
361:
362:
363: 364: 365:
366: protected function getCache()
367: {
368: return Nette\Environment::getCache('Nette.RobotLoader');
369: }
370:
371:
372:
373: 374: 375:
376: protected function getKey()
377: {
378: return md5("v2|$this->ignoreDirs|$this->acceptFiles|" . implode('|', $this->scanDirs));
379: }
380:
381:
382:
383: 384: 385:
386: protected function isProduction()
387: {
388: return Nette\Environment::isProduction();
389: }
390:
391: }
392: