Renamed classes/main/ to main/classes/ + added FuseFeature, an upcoming feature
[core.git] / inc / loader / class_ClassLoader.php
1 <?php
2 /**
3  * This class loads class include files with a specific prefix and suffix
4  *
5  * @author              Roland Haeder <webmaster@shipsimu.org>
6  * @version             0.0.0
7  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2015 Core Developer Team
8  * @license             GNU GPL 3.0 or any newer version
9  * @link                http://www.shipsimu.org
10  *
11  * This program is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public License
22  * along with this program. If not, see <http://www.gnu.org/licenses/>.
23  *
24  * ----------------------------------
25  * 1.4
26  *  - Some comments improved, other minor improvements
27  * 1.3
28  *  - Constructor is now empty and factory method 'createClassLoader' is created
29  *  - renamed loadClasses to scanClassPath
30  *  - Added initLoader()
31  * 1.2
32  *  - ClassLoader rewritten to PHP SPL's own RecursiveIteratorIterator class
33  * 1.1
34  *  - loadClasses rewritten to fix some notices
35  * 1.0
36  *  - Initial release
37  * ----------------------------------
38  */
39 class ClassLoader {
40         /**
41          * Instance of this class
42          */
43         private static $selfInstance = NULL;
44
45         /**
46          * Array with all classes
47          */
48         private $classes = array();
49
50         /**
51          * List of loaded classes
52          */
53         private $loadedClasses = array();
54
55         /**
56          * Suffix with extension for all class files
57          */
58         private $prefix = 'class_';
59
60         /**
61          * Suffix with extension for all class files
62          */
63         private $suffix = '.php';
64
65         /**
66          * A list for directory names (no leading/trailing slashes!) which not be scanned by the path scanner
67          * @see scanLocalPath
68          */
69         private $ignoreList = array();
70
71         /**
72          * Debug this class loader? (TRUE = yes, FALSE = no)
73          */
74         private $debug = FALSE;
75
76         /**
77          * Whether the file list is cached
78          */
79         private $listCached = FALSE;
80
81         /**
82          * Wethe class content has been cached
83          */
84         private $classesCached = FALSE;
85
86         /**
87          * Filename for the list cache
88          */
89         private $listCacheFQFN = '';
90
91         /**
92          * Cache for class content
93          */
94         private $classCacheFQFN = '';
95
96         /**
97          * Counter for loaded include files
98          */
99         private $total = 0;
100
101         /**
102          * Framework/application paths for classes, etc.
103          */
104         private static $frameworkPaths = array(
105                 'exceptions', // Exceptions
106                 'interfaces', // Interfaces
107                 'classes',    // Classes
108                 'middleware'  // The middleware
109         );
110
111
112         /**
113          * The protected constructor. Please use the factory method below, or use
114          * getSelfInstance() for singleton
115          *
116          * @return      void
117          */
118         protected function __construct () {
119                 // Is currently empty
120         }
121
122         /**
123          * The destructor makes it sure all caches got flushed
124          *
125          * @return      void
126          */
127         public function __destruct () {
128                 // Skip here if dev-mode
129                 if (defined('DEVELOPER')) {
130                         return;
131                 } // END - if
132
133                 // Skip here if already cached
134                 if ($this->listCached === FALSE) {
135                         // Writes the cache file of our list away
136                         $cacheContent = json_encode($this->classes);
137                         file_put_contents($this->listCacheFQFN, $cacheContent);
138                 } // END - if
139
140                 // Skip here if already cached
141                 if ($this->classesCached === FALSE) {
142                         // Generate a full-cache of all classes
143                         $cacheContent = '';
144                         foreach ($this->loadedClasses as $fqfn) {
145                                 // Load the file
146                                 $cacheContent .= file_get_contents($fqfn);
147                         } // END - foreach
148
149                         // And write it away
150                         file_put_contents($this->classCacheFQFN, $cacheContent);
151                 } // END - if
152         }
153
154         /**
155          * Creates an instance of this class loader for given configuration instance
156          *
157          * @param       $configInstance         Configuration class instance
158          * @return      void
159          */
160         public static final function createClassLoader (FrameworkConfiguration $configInstance) {
161                 // Get a new instance
162                 $loaderInstance = new ClassLoader();
163
164                 // Init the instance
165                 $loaderInstance->initLoader($configInstance);
166
167                 // Return the prepared instance
168                 return $loaderInstance;
169         }
170
171         /**
172          * Scans for all framework classes, exceptions and interfaces.
173          *
174          * @return      void
175          */
176         public static function scanFrameworkClasses () {
177                 // Cache loader instance
178                 $loaderInstance = self::getSelfInstance();
179
180                 // Load all classes
181                 foreach (self::$frameworkPaths as $pathName) {
182                         // Try to load the framework classes
183                         $loaderInstance->scanClassPath('inc/main/' . $pathName . '/');
184                 } // END - foreach
185         }
186
187         /**
188          * Scans for application's classes, etc.
189          *
190          * @return      void
191          */
192         public static function scanApplicationClasses () {
193                 // Get config instance
194                 $cfg = FrameworkConfiguration::getSelfInstance();
195
196                 // Load all classes for the application
197                 foreach (self::$frameworkPaths as $class) {
198                         // Create path name
199                         $path = sprintf('%s/%s/%s', $cfg->getConfigEntry('application_path'), $cfg->getConfigEntry('app_name'), $class);
200
201                         // Is the path readable?
202                         if (is_dir($path)) {
203                                 // Try to load the application classes
204                                 ClassLoader::getSelfInstance()->scanClassPath($path);
205                         } // END - if
206                 } // END - foreach
207         }
208
209         /**
210          * Initializes our loader class
211          *
212          * @param       $configInstance Configuration class instance
213          * @return      void
214          */
215         protected function initLoader (FrameworkConfiguration $configInstance) {
216                 // Set configuration instance
217                 $this->configInstance = $configInstance;
218
219                 // Construct the FQFN for the cache
220                 if (!defined('DEVELOPER')) {
221                         $this->listCacheFQFN  = $this->configInstance->getConfigEntry('local_db_path') . 'list-' . $this->configInstance->getConfigEntry('app_name') . '.cache';
222                         $this->classCacheFQFN = $this->configInstance->getConfigEntry('local_db_path') . 'class-' . $this->configInstance->getConfigEntry('app_name') . '.cache';
223                 } // END - if
224
225                 // Set suffix and prefix from configuration
226                 $this->suffix = $configInstance->getConfigEntry('class_suffix');
227                 $this->prefix = $configInstance->getConfigEntry('class_prefix');
228
229                 // Set own instance
230                 self::$selfInstance = $this;
231
232                 // Skip here if no dev-mode
233                 if (defined('DEVELOPER')) {
234                         return;
235                 } // END - if
236
237                 // IS the cache there?
238                 if (BaseFrameworkSystem::isReadableFile($this->listCacheFQFN)) {
239                         // Get content
240                         $cacheContent = file_get_contents($this->listCacheFQFN);
241
242                         // And convert it
243                         $this->classes = json_decode($cacheContent);
244
245                         // List has been restored from cache!
246                         $this->listCached = TRUE;
247                 } // END - if
248
249                 // Does the class cache exist?
250                 if (BaseFrameworkSystem::isReadableFile($this->listCacheFQFN)) {
251                         // Then include it
252                         require($this->classCacheFQFN);
253
254                         // Mark the class cache as loaded
255                         $this->classesCached = TRUE;
256                 } // END - if
257         }
258
259         /**
260          * Autoload-function
261          *
262          * @param       $className      Name of the class to load
263          * @return      void
264          */
265         public static function autoLoad ($className) {
266                 // Try to include this class
267                 //* NOISY-DEBUG: */ printf('[%s:%d] className=%s' . PHP_EOL, __METHOD__, __LINE__, $className);
268                 self::getSelfInstance()->includeClass($className);
269         }
270
271         /**
272          * Singleton getter for an instance of this class
273          *
274          * @return      $selfInstance   A singleton instance of this class
275          */
276         public static final function getSelfInstance () {
277                 // Is the instance there?
278                 if (is_null(self::$selfInstance)) {
279                         // Get a new one
280                         self::$selfInstance = ClassLoader::createClassLoader(FrameworkConfiguration::getSelfInstance());
281                 } // END - if
282
283                 // Return the instance
284                 return self::$selfInstance;
285         }
286
287         /**
288          * Scans recursively a local path for class files which must have a prefix and a suffix as given by $this->suffix and $this->prefix
289          *
290          * @param       $basePath               The relative base path to 'base_path' constant for all classes
291          * @param       $ignoreList             An optional list (array forced) of directory and file names which shall be ignored
292          * @return      void
293          */
294         public function scanClassPath ($basePath, array $ignoreList = array() ) {
295                 // Is a list has been restored from cache, don't read it again
296                 if ($this->listCached === TRUE) {
297                         // Abort here
298                         return;
299                 } // END - if
300
301                 // Keep it in class for later usage
302                 $this->ignoreList = $ignoreList;
303
304                 /*
305                  * Ignore .htaccess by default as it is for protection of directories
306                  * on Apache servers.
307                  */
308                 array_push($ignoreList, '.htaccess');
309
310                 /*
311                  * Set base directory which holds all our classes, an absolute path
312                  * should be used here so is_dir(), is_file() and so on will always
313                  * find the correct files and dirs.
314                  */
315                 $basePath2 = realpath($basePath);
316
317                 // If the basePath is FALSE it is invalid
318                 if ($basePath2 === FALSE) {
319                         /* @TODO: Do not exit here. */
320                         exit(__METHOD__ . ': Cannot read ' . $basePath . ' !' . PHP_EOL);
321                 } else {
322                         // Set base path
323                         $basePath = $basePath2;
324                 }
325
326                 // Get a new iterator
327                 //* NOISY-DEBUG: */ printf('[%s:%d] basePath=%s' . PHP_EOL, __METHOD__, __LINE__, $basePath);
328                 $iteratorInstance = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath), RecursiveIteratorIterator::CHILD_FIRST);
329
330                 // Load all entries
331                 while ($iteratorInstance->valid()) {
332                         // Get current entry
333                         $currentEntry = $iteratorInstance->current();
334
335                         // Get filename from iterator
336                         $fileName = $currentEntry->getFileName();
337
338                         // Get the "FQFN" (path and file name)
339                         $fqfn = $currentEntry->getRealPath();
340
341                         // Current entry must be a file, not smaller than 100 bytes and not on ignore list 
342                         if ((!$currentEntry->isFile()) || (in_array($fileName, $this->ignoreList)) || (filesize($fqfn) < 100)) {
343                                 // Advance to next entry
344                                 $iteratorInstance->next();
345
346                                 // Skip non-file entries
347                                 //* NOISY-DEBUG: */ printf('[%s:%d] SKIP: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
348                                 continue;
349                         } // END - if
350
351                         // Is this file wanted?
352                         //* NOISY-DEBUG: */ printf('[%s:%d] FOUND: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
353                         if ((substr($fileName, 0, strlen($this->prefix)) == $this->prefix) && (substr($fileName, -strlen($this->suffix), strlen($this->suffix)) == $this->suffix)) {
354                                 // Add it to the list
355                                 //* NOISY-DEBUG: */ printf('[%s:%d] ADD: %s,fqfn=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $fqfn);
356                                 $this->classes[$fileName] = $fqfn;
357                         } else {
358                                 // Not added
359                                 //* NOISY-DEBUG: */ printf('[%s:%d] NOT ADDED: %s,fqfn=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $fqfn);
360                         }
361
362                         // Advance to next entry
363                         //* NOISY-DEBUG: */ printf('[%s:%d] NEXT: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
364                         $iteratorInstance->next();
365                 } // END - while
366         }
367
368         /**
369          * Load extra config files
370          *
371          * @return      void
372          */
373         public function loadExtraConfigs () {
374                 // Backup old prefix
375                 $oldPrefix = $this->prefix;
376
377                 // Set new prefix (temporary!)
378                 $this->prefix = 'config-';
379
380                 // Set base directory
381                 $basePath = $this->configInstance->getConfigEntry('base_path') . 'inc/config/';
382
383                 // Load all classes from the config directory
384                 $this->scanClassPath($basePath);
385
386                 // Include these extra configs now
387                 $this->includeExtraConfigs();
388
389                 // Set back the old prefix
390                 $this->prefix = $oldPrefix;
391         }
392
393         /**
394          * Tries to find the given class in our list. This method ignores silently
395          * missing classes or interfaces. So if you use class_exists() this method
396          * does not interrupt your program.
397          *
398          * @param       $className      The class that shall be loaded
399          * @return      void
400          */
401         public function includeClass ($className) {
402                 // Create a name with prefix and suffix
403                 $fileName = $this->prefix . $className . $this->suffix;
404
405                 // Now look it up in our index
406                 //* NOISY-DEBUG: */ printf('[%s:%d] ISSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
407                 if ((isset($this->classes[$fileName])) && (!in_array($this->classes[$fileName], $this->loadedClasses))) {
408                         // File is found and not loaded so load it only once
409                         //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName);
410                         require($this->classes[$fileName]);
411                         //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - END' . PHP_EOL, __METHOD__, __LINE__, $fileName);
412
413                         // Count this loaded class/interface/exception
414                         $this->total++;
415
416                         // Mark this class as loaded for other purposes than loading it.
417                         array_push($this->loadedClasses, $this->classes[$fileName]);
418
419                         // Remove it from classes list so it won't be found twice.
420                         //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
421                         unset($this->classes[$fileName]);
422
423                         // Developer mode excludes caching (better debugging)
424                         if (!defined('DEVELOPER')) {
425                                 // Reset cache
426                                 //* NOISY-DEBUG: */ printf('[%s:%d] classesCached=FALSE' . PHP_EOL, __METHOD__, __LINE__);
427                                 $this->classesCached = FALSE;
428                         } // END - if
429                 } else {
430                         // Not found
431                         //* NOISY-DEBUG: */ printf('[%s:%d] 404: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
432                 }
433         }
434
435         /**
436          * Includes all extra config files
437          *
438          * @return      void
439          */
440         private function includeExtraConfigs () {
441                 // Run through all class names (should not be much)
442                 foreach ($this->classes as $fileName => $fqfn) {
443                         // Is this a config?
444                         if (substr($fileName, 0, strlen($this->prefix)) == $this->prefix) {
445                                 // Then include it
446                                 require($fqfn);
447
448                                 // Remove it from the list
449                                 //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
450                                 unset($this->classes[$fileName]);
451                         } // END - if
452                 } // END - foreach
453         }
454
455         /**
456          * Getter for total include counter
457          *
458          * @return      $total  Total loaded include files
459          */
460         public final function getTotal () {
461                 return $this->total;
462         }
463
464         /**
465          * Getter for a printable list of included main/interfaces/exceptions
466          *
467          * @param       $includeList    A printable include list
468          */
469         public function getPrintableIncludeList () {
470                 // Prepare the list
471                 $includeList = '';
472                 foreach ($this->loadedClasses as $classFile) {
473                         $includeList .= basename($classFile) . '<br />' . PHP_EOL;
474                 } // END - foreach
475
476                 // And return it
477                 return $includeList;
478         }
479 }
480
481 // [EOF]
482 ?>