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