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