]> git.mxchange.org Git - core.git/blob - framework/loader/class_ClassLoader.php
strict naming-convention check can be disabled, but is not recommended in
[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                 // Cache 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 config instance
255                 $cfg = FrameworkConfiguration::getSelfInstance();
256
257                 // Load all classes for the application
258                 foreach (self::$frameworkPaths as $shortPath) {
259                         // Debug message
260                         //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath);
261
262                         // Create path name
263                         $pathName = realpath(sprintf(
264                                 '%s/%s/%s',
265                                 $cfg->getConfigEntry('application_path'),
266                                 $cfg->getConfigEntry('app_name'),
267                                 $shortPath
268                         ));
269
270                         // Debug message
271                         //* NOISY-DEBUG: */ printf('[%s:%d]: pathName[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($pathName), $pathName);
272
273                         // Is the path readable?
274                         if (is_dir($pathName)) {
275                                 // Try to load the application classes
276                                 ClassLoader::getSelfInstance()->scanClassPath($pathName);
277                         } // END - if
278                 } // END - foreach
279
280                 // Trace message
281                 //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__);
282         }
283
284         /**
285          * Scans for test classes, etc.
286          *
287          * @return      void
288          */
289         public static function scanTestsClasses () {
290                 // Trace message
291                 //* NOISY-DEBUG: */ printf('[%s:%d]: CALLED!' . PHP_EOL, __METHOD__, __LINE__);
292
293                 // Get config instance
294                 $cfg = FrameworkConfiguration::getSelfInstance();
295
296                 // Load all classes for the application
297                 foreach (self::$testPaths as $shortPath) {
298                         // Debug message
299                         //* NOISY-DEBUG: */ printf('[%s:%d]: shortPath=%s' . PHP_EOL, __METHOD__, __LINE__, $shortPath);
300
301                         // Create path name
302                         $pathName = realpath(sprintf(
303                                 '%s/%s',
304                                 $cfg->getConfigEntry('base_path'),
305                                 $shortPath
306                         ));
307
308                         // Debug message
309                         //* NOISY-DEBUG: */ printf('[%s:%d]: pathName[%s]=%s' . PHP_EOL, __METHOD__, __LINE__, gettype($pathName), $pathName);
310
311                         // Is the path readable?
312                         if (is_dir($pathName)) {
313                                 // Try to load the application classes
314                                 ClassLoader::getSelfInstance()->scanClassPath($pathName);
315                         } // END - if
316                 } // END - foreach
317
318                 // Trace message
319                 //* NOISY-DEBUG: */ printf('[%s:%d]: EXIT!' . PHP_EOL, __METHOD__, __LINE__);
320         }
321
322         /**
323          * Enables or disables strict naming-convention tests on class loading
324          *
325          * @param       $strictNamingConventionCheck    Whether to strictly check naming-convention
326          * @return      void
327          */
328         public static function enableStrictNamingConventionCheck ($strictNamingConventionCheck = TRUE) {
329                 self::$strictNamingConventionCheck = $strictNamingConventionCheck;
330         }
331
332         /**
333          * Registeres given relative path where test classes reside. For regular
334          * framework uses, they should not be loaded (and used).
335          *
336          * @param       $relativePath   Relative path to test classes
337          * @return      void
338          */
339         public static function registerTestsPath ($relativePath) {
340                 // "Register" it
341                 self::$testPaths[$relativePath] = $relativePath;
342         }
343
344         /**
345          * Autoload-function
346          *
347          * @param       $className      Name of the class to load
348          * @return      void
349          */
350         public static function autoLoad ($className) {
351                 // Try to include this class
352                 self::getSelfInstance()->loadClassFile($className);
353         }
354
355         /**
356          * Singleton getter for an instance of this class
357          *
358          * @return      $selfInstance   A singleton instance of this class
359          */
360         public static final function getSelfInstance () {
361                 // Is the instance there?
362                 if (is_null(self::$selfInstance)) {
363                         // Get a new one
364                         self::$selfInstance = ClassLoader::createClassLoader(FrameworkConfiguration::getSelfInstance());
365                 } // END - if
366
367                 // Return the instance
368                 return self::$selfInstance;
369         }
370
371         /**
372          * Scans recursively a local path for class files which must have a prefix and a suffix as given by $this->suffix and $this->prefix
373          *
374          * @param       $basePath               The relative base path to 'base_path' constant for all classes
375          * @param       $ignoreList             An optional list (array forced) of directory and file names which shall be ignored
376          * @return      void
377          */
378         public function scanClassPath ($basePath, array $ignoreList = array() ) {
379                 // Is a list has been restored from cache, don't read it again
380                 if ($this->listCached === TRUE) {
381                         // Abort here
382                         return;
383                 } // END - if
384
385                 // Keep it in class for later usage
386                 $this->ignoreList = $ignoreList;
387
388                 /*
389                  * Ignore .htaccess by default as it is for protection of directories
390                  * on Apache servers.
391                  */
392                 array_push($ignoreList, '.htaccess');
393
394                 /*
395                  * Set base directory which holds all our classes, an absolute path
396                  * should be used here so is_dir(), is_file() and so on will always
397                  * find the correct files and dirs.
398                  */
399                 $basePath2 = realpath($basePath);
400
401                 // If the basePath is FALSE it is invalid
402                 if ($basePath2 === FALSE) {
403                         /* @TODO: Do not exit here. */
404                         exit(__METHOD__ . ': Cannot read ' . $basePath . ' !' . PHP_EOL);
405                 } else {
406                         // Set base path
407                         $basePath = $basePath2;
408                 }
409
410                 // Get a new iterator
411                 //* NOISY-DEBUG: */ printf('[%s:%d] basePath=%s' . PHP_EOL, __METHOD__, __LINE__, $basePath);
412                 $iteratorInstance = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basePath), RecursiveIteratorIterator::CHILD_FIRST);
413
414                 // Load all entries
415                 while ($iteratorInstance->valid()) {
416                         // Get current entry
417                         $currentEntry = $iteratorInstance->current();
418
419                         // Get filename from iterator
420                         $fileName = $currentEntry->getFileName();
421
422                         // Get the "FQFN" (path and file name)
423                         $fqfn = $currentEntry->getRealPath();
424
425                         // Current entry must be a file, not smaller than 100 bytes and not on ignore list 
426                         if ((!$currentEntry->isFile()) || (in_array($fileName, $this->ignoreList)) || (filesize($fqfn) < 100)) {
427                                 // Advance to next entry
428                                 $iteratorInstance->next();
429
430                                 // Skip non-file entries
431                                 //* NOISY-DEBUG: */ printf('[%s:%d] SKIP: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
432                                 continue;
433                         } // END - if
434
435                         // Is this file wanted?
436                         //* NOISY-DEBUG: */ printf('[%s:%d] FOUND: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
437                         if ((substr($fileName, 0, strlen($this->prefix)) == $this->prefix) && (substr($fileName, -strlen($this->suffix), strlen($this->suffix)) == $this->suffix)) {
438                                 // Add it to the list
439                                 //* NOISY-DEBUG: */ printf('[%s:%d] ADD: %s,fqfn=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $fqfn);
440                                 $this->foundClasses[$fileName] = $fqfn;
441                         } else {
442                                 // Not added
443                                 //* NOISY-DEBUG: */ printf('[%s:%d] NOT ADDED: %s,fqfn=%s' . PHP_EOL, __METHOD__, __LINE__, $fileName, $fqfn);
444                         }
445
446                         // Advance to next entry
447                         //* NOISY-DEBUG: */ printf('[%s:%d] NEXT: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
448                         $iteratorInstance->next();
449                 } // END - while
450         }
451
452         /**
453          * Load extra config files
454          *
455          * @return      void
456          */
457         public function loadExtraConfigs () {
458                 // Backup old prefix
459                 $oldPrefix = $this->prefix;
460
461                 // Set new prefix (temporary!)
462                 $this->prefix = 'config-';
463
464                 // Set base directory
465                 $basePath = $this->configInstance->getConfigEntry('base_path') . 'framework/config/';
466
467                 // Load all classes from the config directory
468                 $this->scanClassPath($basePath);
469
470                 // Include these extra configs now
471                 $this->includeExtraConfigs();
472
473                 // Set back the old prefix
474                 $this->prefix = $oldPrefix;
475         }
476
477         /**
478          * Initializes our loader class
479          *
480          * @param       $configInstance Configuration class instance
481          * @return      void
482          */
483         private function initLoader (FrameworkConfiguration $configInstance) {
484                 // Set configuration instance
485                 $this->configInstance = $configInstance;
486
487                 // Construct the FQFN for the cache
488                 if (!defined('DEVELOPER')) {
489                         $this->listCacheFQFN  = $this->configInstance->getConfigEntry('local_db_path') . 'list-' . $this->configInstance->getConfigEntry('app_name') . '.cache';
490                         $this->classCacheFQFN = $this->configInstance->getConfigEntry('local_db_path') . 'class-' . $this->configInstance->getConfigEntry('app_name') . '.cache';
491                 } // END - if
492
493                 // Set suffix and prefix from configuration
494                 $this->suffix = $configInstance->getConfigEntry('class_suffix');
495                 $this->prefix = $configInstance->getConfigEntry('class_prefix');
496
497                 // Set own instance
498                 self::$selfInstance = $this;
499
500                 // Skip here if no dev-mode
501                 if (defined('DEVELOPER')) {
502                         return;
503                 } // END - if
504
505                 // IS the cache there?
506                 if (BaseFrameworkSystem::isReadableFile($this->listCacheFQFN)) {
507                         // Get content
508                         $cacheContent = file_get_contents($this->listCacheFQFN);
509
510                         // And convert it
511                         $this->foundClasses = json_decode($cacheContent);
512
513                         // List has been restored from cache!
514                         $this->listCached = TRUE;
515                 } // END - if
516
517                 // Does the class cache exist?
518                 if (BaseFrameworkSystem::isReadableFile($this->listCacheFQFN)) {
519                         // Then include it
520                         require($this->classCacheFQFN);
521
522                         // Mark the class cache as loaded
523                         $this->classesCached = TRUE;
524                 } // END - if
525         }
526
527         /**
528          * Tries to find the given class in our list. This method ignores silently
529          * missing classes or interfaces. So if you use class_exists() this method
530          * does not interrupt your program.
531          *
532          * @param       $className      The class that shall be loaded
533          * @return      void
534          * @throws      InvalidArgumentException        If strict-checking is enabled and class name is not following naming convention
535          */
536         private function loadClassFile ($className) {
537                 // Trace message
538                 //* NOISY-DEBUG: */ printf('[%s:%d] className=%s - CALLED!' . PHP_EOL, __METHOD__, __LINE__, $className);
539
540                 // The class name should contain at least 2 back-slashes, so split at them
541                 $classNameParts = explode("\\", $className);
542
543                 // At least 3 parts should be there
544                 if ((self::$strictNamingConventionCheck === TRUE) && (count($classNameParts) < 3)) {
545                         // Namespace scheme is: Project\Package[\SubPackage...]
546                         throw new InvalidArgumentException(sprintf('Class name "%s" is not conform to naming-convention: Project\Package[\SubPackage...]\SomeFooBar', $className));
547                 } // END - if
548
549                 // Get last element
550                 $shortClassName = array_pop($classNameParts);
551
552                 // Create a name with prefix and suffix
553                 $fileName = $this->prefix . $shortClassName . $this->suffix;
554
555                 // Now look it up in our index
556                 //* NOISY-DEBUG: */ printf('[%s:%d] ISSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
557                 if ((isset($this->foundClasses[$fileName])) && (!isset($this->loadedClasses[$this->foundClasses[$fileName]]))) {
558                         // File is found and not loaded so load it only once
559                         //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName);
560                         require($this->foundClasses[$fileName]);
561                         //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - END' . PHP_EOL, __METHOD__, __LINE__, $fileName);
562
563                         // Count this loaded class/interface/exception
564                         $this->total++;
565
566                         // Mark this class as loaded for other purposes than loading it.
567                         $this->loadedClasses[$this->foundClasses[$fileName]] = TRUE;
568
569                         // Remove it from classes list so it won't be found twice.
570                         //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
571                         unset($this->foundClasses[$fileName]);
572
573                         // Developer mode excludes caching (better debugging)
574                         if (!defined('DEVELOPER')) {
575                                 // Reset cache
576                                 //* NOISY-DEBUG: */ printf('[%s:%d] classesCached=FALSE' . PHP_EOL, __METHOD__, __LINE__);
577                                 $this->classesCached = FALSE;
578                         } // END - if
579                 } else {
580                         // Not found
581                         //* NOISY-DEBUG: */ printf('[%s:%d] 404: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
582                 }
583         }
584
585         /**
586          * Includes all extra config files
587          *
588          * @return      void
589          */
590         private function includeExtraConfigs () {
591                 // Run through all class names (should not be much)
592                 foreach ($this->foundClasses as $fileName => $fqfn) {
593                         // Is this a config?
594                         if (substr($fileName, 0, strlen($this->prefix)) == $this->prefix) {
595                                 // Then include it
596                                 //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - START' . PHP_EOL, __METHOD__, __LINE__, $fileName);
597                                 require($fqfn);
598                                 //* NOISY-DEBUG: */ printf('[%s:%d] LOAD: %s - END' . PHP_EOL, __METHOD__, __LINE__, $fileName);
599
600                                 // Remove it from the list
601                                 //* NOISY-DEBUG: */ printf('[%s:%d] UNSET: %s' . PHP_EOL, __METHOD__, __LINE__, $fileName);
602                                 unset($this->foundClasses[$fileName]);
603                         } // END - if
604                 } // END - foreach
605         }
606
607         /**
608          * Getter for total include counter
609          *
610          * @return      $total  Total loaded include files
611          */
612         public final function getTotal () {
613                 return $this->total;
614         }
615
616         /**
617          * Getter for a printable list of included main/interfaces/exceptions
618          *
619          * @param       $includeList    A printable include list
620          */
621         public function getPrintableIncludeList () {
622                 // Prepare the list
623                 $includeList = '';
624                 foreach ($this->loadedClasses as $classFile) {
625                         $includeList .= basename($classFile) . '<br />' . PHP_EOL;
626                 } // END - foreach
627
628                 // And return it
629                 return $includeList;
630         }
631
632 }