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