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