Proper way of searching for paths
[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          * Framework/application paths for classes, etc.
120          */
121         private static $frameworkPaths = array(
122                 'exceptions', // Exceptions
123                 'interfaces', // Interfaces
124                 'classes',    // Classes
125                 'middleware'  // The middleware
126         );
127
128         /**
129          * Registered paths where test classes can be found. These are all relative
130          * to base_path .
131          */
132         private static $testPaths = array();
133
134         /**
135          * The protected constructor. Please use the factory method below, or use
136          * getSelfInstance() for singleton
137          *
138          * @return      void
139          */
140         protected function __construct () {
141                 // This is empty for now
142         }
143
144         /**
145          * The destructor makes it sure all caches got flushed
146          *
147          * @return      void
148          */
149         public function __destruct () {
150                 // Skip here if dev-mode
151                 if (defined('DEVELOPER')) {
152                         return;
153                 } // END - if
154
155                 // Skip here if already cached
156                 if ($this->listCached === FALSE) {
157                         // Writes the cache file of our list away
158                         $cacheContent = json_encode($this->foundClasses);
159                         file_put_contents($this->listCacheFQFN, $cacheContent);
160                 } // END - if
161
162                 // Skip here if already cached
163                 if ($this->classesCached === FALSE) {
164                         // Generate a full-cache of all classes
165                         $cacheContent = '';
166                         foreach (array_keys($this->loadedClasses) as $fqfn) {
167                                 // Load the file
168                                 $cacheContent .= file_get_contents($fqfn);
169                         } // END - foreach
170
171                         // And write it away
172                         file_put_contents($this->classCacheFQFN, $cacheContent);
173                 } // END - if
174         }
175
176         /**
177          * Creates an instance of this class loader for given configuration instance
178          *
179          * @param       $configInstance         Configuration class instance
180          * @return      void
181          */
182         public static final function createClassLoader (FrameworkConfiguration $configInstance) {
183                 // Get a new instance
184                 $loaderInstance = new ClassLoader();
185
186                 // Init the instance
187                 $loaderInstance->initLoader($configInstance);
188
189                 // Return the prepared instance
190                 return $loaderInstance;
191         }
192
193         /**
194          * Scans for all framework classes, exceptions and interfaces.
195          *
196          * @return      void
197          */
198         public static function scanFrameworkClasses () {
199                 // Trace message
200                 //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__);
201
202                 // Cache loader instance
203                 $loaderInstance = self::getSelfInstance();
204
205                 // Get config instance
206                 $cfg = FrameworkConfiguration::getSelfInstance();
207
208                 // Load all classes
209                 foreach (self::$frameworkPaths as $shortPath) {
210                         // Debug message
211                         //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath);
212
213                         // Generate full path from it
214                         $pathName = realpath(sprintf(
215                                 '%s/framework/main/%s/',
216                                 $cfg->getConfigEntry('base_path'),
217                                 $shortPath
218                         ));
219
220                         // Debug message
221                         //* NOISY-DEBUG: */ printf('[%s:%d]: pathName=%s' . PHP_EOL, __METHOD__, __LINE__, $pathName);
222
223                         // Is it not FALSE and accessible?
224                         if (is_bool($pathName)) {
225                                 // Skip this
226                                 continue;
227                         } elseif (!is_readable($pathName)) {
228                                 // @TODO Throw exception instead of break
229                                 break;
230                         }
231
232                         // Try to load the framework classes
233                         $loaderInstance->scanClassPath($pathName);
234                 } // END - foreach
235
236                 // Trace message
237                 //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__);
238         }
239
240         /**
241          * Scans for application's classes, etc.
242          *
243          * @return      void
244          */
245         public static function scanApplicationClasses () {
246                 // Trace message
247                 //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__);
248
249                 // Get config instance
250                 $cfg = FrameworkConfiguration::getSelfInstance();
251
252                 // Load all classes for the application
253                 foreach (self::$frameworkPaths as $shortPath) {
254                         // Debug message
255                         //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath);
256
257                         // Create path name
258                         $pathName = realpath(sprintf(
259                                 '%s/%s/%s',
260                                 $cfg->getConfigEntry('application_path'),
261                                 $cfg->getConfigEntry('app_name'),
262                                 $shortPath
263                         ));
264
265                         // Debug message
266                         //* NOISY-DEBUG: */ printf('[%s:%d]: pathName[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($pathName), $pathName);
267
268                         // Is the path readable?
269                         if (is_dir($pathName)) {
270                                 // Try to load the application classes
271                                 ClassLoader::getSelfInstance()->scanClassPath($pathName);
272                         } // END - if
273                 } // END - foreach
274
275                 // Trace message
276                 //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__);
277         }
278
279         /**
280          * Scans for test classes, etc.
281          *
282          * @return      void
283          */
284         public static function scanTestsClasses () {
285                 // Trace message
286                 //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__);
287
288                 // Get config instance
289                 $cfg = FrameworkConfiguration::getSelfInstance();
290
291                 // Load all classes for the application
292                 foreach (self::$testPaths as $shortPath) {
293                         // Debug message
294                         //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath);
295
296                         // Create path name
297                         $pathName = realpath(sprintf(
298                                 '%s/%s',
299                                 $cfg->getConfigEntry('base_path'),
300                                 $shortPath
301                         ));
302
303                         // Debug message
304                         //* NOISY-DEBUG: */ printf('[%s:%d]: pathName[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($pathName), $pathName);
305
306                         // Is the path readable?
307                         if (is_dir($pathName)) {
308                                 // Try to load the application classes
309                                 ClassLoader::getSelfInstance()->scanClassPath($pathName);
310                         } // END - if
311                 } // END - foreach
312
313                 // Trace message
314                 //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__);
315         }
316
317         /**
318          * Registeres given relative path where test classes reside. For regular
319          * framework uses, they should not be loaded (and used).
320          *
321          * @param       $relativePath   Relative path to test classes
322          * @return      void
323          */
324         public static function registerTestsPath ($relativePath) {
325                 // "Register" it
326                 self::$testPaths[$relativePath] = $relativePath;
327         }
328
329         /**
330          * Autoload-function
331          *
332          * @param       $className      Name of the class to load
333          * @return      void
334          */
335         public static function autoLoad ($className) {
336                 // Try to include this class
337                 self::getSelfInstance()->loadClassFile($className);
338         }
339
340         /**
341          * Singleton getter for an instance of this class
342          *
343          * @return      $selfInstance   A singleton instance of this class
344          */
345         public static final function getSelfInstance () {
346                 // Is the instance there?
347                 if (is_null(self::$selfInstance)) {
348                         // Get a new one
349                         self::$selfInstance = ClassLoader::createClassLoader(FrameworkConfiguration::getSelfInstance());
350                 } // END - if
351
352                 // Return the instance
353                 return self::$selfInstance;
354         }
355
356         /**
357          * Scans recursively a local path for class files which must have a prefix and a suffix as given by $this->suffix and $this->prefix
358          *
359          * @param       $basePath               The relative base path to 'base_path' constant for all classes
360          * @param       $ignoreList             An optional list (array forced) of directory and file names which shall be ignored
361          * @return      void
362          */
363         public function scanClassPath ($basePath, array $ignoreList = array() ) {
364                 // Is a list has been restored from cache, don't read it again
365                 if ($this->listCached === TRUE) {
366                         // Abort here
367                         return;
368                 } // END - if
369
370                 // Keep it in class for later usage
371                 $this->ignoreList = $ignoreList;
372
373                 /*
374                  * Ignore .htaccess by default as it is for protection of directories
375                  * on Apache servers.
376                  */
377                 array_push($ignoreList, '.htaccess');
378
379                 /*
380                  * Set base directory which holds all our classes, an absolute path
381                  * should be used here so is_dir(), is_file() and so on will always
382                  * find the correct files and dirs.
383                  */
384                 $basePath2 = realpath($basePath);
385
386                 // If the basePath is FALSE it is invalid
387                 if ($basePath2 === FALSE) {
388                         /* @TODO: Do not exit here. */
389                         exit(__METHOD__ . ': Cannot read ' . $basePath . ' !' . PHP_EOL);
390                 } else {
391                         // Set base path
392                         $basePath = $basePath2;
393                 }
394
395                 // Get a new iterator
396                 //* NOISY-DEBUG: */ printf('[%s:%d] basePath=%s' . PHP_EOL, __METHOD__, __LINE__, $basePath);
397                 $iteratorInstance = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath), RecursiveIteratorIterator::CHILD_FIRST);
398
399                 // Load all entries
400                 while ($iteratorInstance->valid()) {
401                         // Get current entry
402                         $currentEntry = $iteratorInstance->current();
403
404                         // Get filename from iterator
405                         $fileName = $currentEntry->getFileName();
406
407                         // Get the "FQFN" (path and file name)
408                         $fqfn = $currentEntry->getRealPath();
409
410                         // Current entry must be a file, not smaller than 100 bytes and not on ignore list 
411                         if ((!$currentEntry->isFile()) || (in_array($fileName, $this->ignoreList)) || (filesize($fqfn) < 100)) {
412                                 // Advance to next entry
413                                 $iteratorInstance->next();
414
415                                 // Skip non-file entries
416                                 //* NOISY-DEBUG: */ printf('[%s:%d] SKIP: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
417                                 continue;
418                         } // END - if
419
420                         // Is this file wanted?
421                         //* NOISY-DEBUG: */ printf('[%s:%d] FOUND: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
422                         if ((substr($fileName, 0, strlen($this->prefix)) == $this->prefix) && (substr($fileName, -strlen($this->suffix), strlen($this->suffix)) == $this->suffix)) {
423                                 // Add it to the list
424                                 //* NOISY-DEBUG: */ printf('[%s:%d] ADD: %s,fqfn=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $fqfn);
425                                 $this->foundClasses[$fileName] = $fqfn;
426                         } else {
427                                 // Not added
428                                 //* NOISY-DEBUG: */ printf('[%s:%d] NOT ADDED: %s,fqfn=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $fqfn);
429                         }
430
431                         // Advance to next entry
432                         //* NOISY-DEBUG: */ printf('[%s:%d] NEXT: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
433                         $iteratorInstance->next();
434                 } // END - while
435         }
436
437         /**
438          * Load extra config files
439          *
440          * @return      void
441          */
442         public function loadExtraConfigs () {
443                 // Backup old prefix
444                 $oldPrefix = $this->prefix;
445
446                 // Set new prefix (temporary!)
447                 $this->prefix = 'config-';
448
449                 // Set base directory
450                 $basePath = $this->configInstance->getConfigEntry('base_path') . 'framework/config/';
451
452                 // Load all classes from the config directory
453                 $this->scanClassPath($basePath);
454
455                 // Include these extra configs now
456                 $this->includeExtraConfigs();
457
458                 // Set back the old prefix
459                 $this->prefix = $oldPrefix;
460         }
461
462         /**
463          * Initializes our loader class
464          *
465          * @param       $configInstance Configuration class instance
466          * @return      void
467          */
468         private function initLoader (FrameworkConfiguration $configInstance) {
469                 // Set configuration instance
470                 $this->configInstance = $configInstance;
471
472                 // Construct the FQFN for the cache
473                 if (!defined('DEVELOPER')) {
474                         $this->listCacheFQFN  = $this->configInstance->getConfigEntry('local_db_path') . 'list-' . $this->configInstance->getConfigEntry('app_name') . '.cache';
475                         $this->classCacheFQFN = $this->configInstance->getConfigEntry('local_db_path') . 'class-' . $this->configInstance->getConfigEntry('app_name') . '.cache';
476                 } // END - if
477
478                 // Set suffix and prefix from configuration
479                 $this->suffix = $configInstance->getConfigEntry('class_suffix');
480                 $this->prefix = $configInstance->getConfigEntry('class_prefix');
481
482                 // Set own instance
483                 self::$selfInstance = $this;
484
485                 // Skip here if no dev-mode
486                 if (defined('DEVELOPER')) {
487                         return;
488                 } // END - if
489
490                 // IS the cache there?
491                 if (BaseFrameworkSystem::isReadableFile($this->listCacheFQFN)) {
492                         // Get content
493                         $cacheContent = file_get_contents($this->listCacheFQFN);
494
495                         // And convert it
496                         $this->foundClasses = json_decode($cacheContent);
497
498                         // List has been restored from cache!
499                         $this->listCached = TRUE;
500                 } // END - if
501
502                 // Does the class cache exist?
503                 if (BaseFrameworkSystem::isReadableFile($this->listCacheFQFN)) {
504                         // Then include it
505                         require($this->classCacheFQFN);
506
507                         // Mark the class cache as loaded
508                         $this->classesCached = TRUE;
509                 } // END - if
510         }
511
512         /**
513          * Tries to find the given class in our list. This method ignores silently
514          * missing classes or interfaces. So if you use class_exists() this method
515          * does not interrupt your program.
516          *
517          * @param       $className      The class that shall be loaded
518          * @return      void
519          */
520         private function loadClassFile ($className) {
521                 // Trace message
522                 //* NOISY-DEBUG: */ printf('[%s:%d] className=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $className);
523
524                 // The class name should contain at least 2 back-slashes, so split at them
525                 $classNameParts = explode("\\", $className);
526
527                 // At least 3 parts should be there
528                 if (count($classNameParts) < 3) {
529                         // Namespace scheme is: Project\Package[\SubPackage...]
530                         throw new InvalidArgumentException(sprintf('Class name "%s" is not conform to naming-convention: Project\Package[\SubPackage...]\SomeFooBar', $className));
531                 } // END - if
532
533                 // Get last element
534                 $shortClassName = array_pop($classNameParts);
535
536                 // Create a name with prefix and suffix
537                 $fileName = $this->prefix . $shortClassName . $this->suffix;
538
539                 // Now look it up in our index
540                 //* NOISY-DEBUG: */ printf('[%s:%d] ISSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
541                 if ((isset($this->foundClasses[$fileName])) && (!isset($this->loadedClasses[$this->foundClasses[$fileName]]))) {
542                         // File is found and not loaded so load it only once
543                         //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName);
544                         require($this->foundClasses[$fileName]);
545                         //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - END' . PHP_EOL, __METHOD__, __LINE__, $fileName);
546
547                         // Count this loaded class/interface/exception
548                         $this->total++;
549
550                         // Mark this class as loaded for other purposes than loading it.
551                         $this->loadedClasses[$this->foundClasses[$fileName]] = TRUE;
552
553                         // Remove it from classes list so it won't be found twice.
554                         //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
555                         unset($this->foundClasses[$fileName]);
556
557                         // Developer mode excludes caching (better debugging)
558                         if (!defined('DEVELOPER')) {
559                                 // Reset cache
560                                 //* NOISY-DEBUG: */ printf('[%s:%d] classesCached=FALSE' . PHP_EOL, __METHOD__, __LINE__);
561                                 $this->classesCached = FALSE;
562                         } // END - if
563                 } else {
564                         // Not found
565                         //* NOISY-DEBUG: */ printf('[%s:%d] 404: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
566                 }
567         }
568
569         /**
570          * Includes all extra config files
571          *
572          * @return      void
573          */
574         private function includeExtraConfigs () {
575                 // Run through all class names (should not be much)
576                 foreach ($this->foundClasses as $fileName => $fqfn) {
577                         // Is this a config?
578                         if (substr($fileName, 0, strlen($this->prefix)) == $this->prefix) {
579                                 // Then include it
580                                 //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName);
581                                 require($fqfn);
582                                 //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - END' . PHP_EOL, __METHOD__, __LINE__, $fileName);
583
584                                 // Remove it from the list
585                                 //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
586                                 unset($this->foundClasses[$fileName]);
587                         } // END - if
588                 } // END - foreach
589         }
590
591         /**
592          * Getter for total include counter
593          *
594          * @return      $total  Total loaded include files
595          */
596         public final function getTotal () {
597                 return $this->total;
598         }
599
600         /**
601          * Getter for a printable list of included main/interfaces/exceptions
602          *
603          * @param       $includeList    A printable include list
604          */
605         public function getPrintableIncludeList () {
606                 // Prepare the list
607                 $includeList = '';
608                 foreach ($this->loadedClasses as $classFile) {
609                         $includeList .= basename($classFile) . '<br />' . PHP_EOL;
610                 } // END - foreach
611
612                 // And return it
613                 return $includeList;
614         }
615
616 }