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