├── src ├── Debug │ ├── AutoloadCollection.php │ ├── icons │ │ └── autoload.svg │ └── AutoloadCollector.php ├── Preloader.php ├── Locator.php └── Autoloader.php ├── README.md ├── LICENSE └── composer.json /src/Debug/AutoloadCollection.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Autoload\Debug; 11 | 12 | use Framework\Debug\Collection; 13 | 14 | /** 15 | * Class AutoloadCollection. 16 | * 17 | * @package autoload 18 | */ 19 | class AutoloadCollection extends Collection 20 | { 21 | protected string $iconPath = __DIR__ . '/icons/autoload.svg'; 22 | } 23 | -------------------------------------------------------------------------------- /src/Debug/icons/autoload.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Aplus Framework Autoload Library 2 | 3 | # Aplus Framework Autoload Library 4 | 5 | - [Home](https://aplus-framework.com/packages/autoload) 6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/autoload/index.html) 7 | - [API Documentation](https://docs.aplus-framework.com/packages/autoload.html) 8 | 9 | [![tests](https://github.com/aplus-framework/autoload/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/autoload/actions/workflows/tests.yml) 10 | [![coverage](https://coveralls.io/repos/github/aplus-framework/autoload/badge.svg?branch=master)](https://coveralls.io/github/aplus-framework/autoload?branch=master) 11 | [![packagist](https://img.shields.io/packagist/v/aplus/autoload)](https://packagist.org/packages/aplus/autoload) 12 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](https://aplus-framework.com/sponsor) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Natan Felles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aplus/autoload", 3 | "description": "Aplus Framework Autoload Library", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "autoload", 8 | "autoloader", 9 | "loader", 10 | "locator", 11 | "file-locator", 12 | "class-locator" 13 | ], 14 | "authors": [ 15 | { 16 | "name": "Natan Felles", 17 | "email": "natanfelles@gmail.com", 18 | "homepage": "https://natanfelles.github.io" 19 | } 20 | ], 21 | "homepage": "https://aplus-framework.com/packages/autoload", 22 | "support": { 23 | "email": "support@aplus-framework.com", 24 | "issues": "https://github.com/aplus-framework/autoload/issues", 25 | "forum": "https://aplus-framework.com/forum", 26 | "source": "https://github.com/aplus-framework/autoload", 27 | "docs": "https://docs.aplus-framework.com/guides/libraries/autoload/" 28 | }, 29 | "funding": [ 30 | { 31 | "type": "Aplus Sponsor", 32 | "url": "https://aplus-framework.com/sponsor" 33 | } 34 | ], 35 | "require": { 36 | "php": ">=8.3", 37 | "ext-intl": "*", 38 | "aplus/debug": "^4.3" 39 | }, 40 | "require-dev": { 41 | "ext-xdebug": "*", 42 | "aplus/coding-standard": "^2.8", 43 | "ergebnis/composer-normalize": "^2.25", 44 | "jetbrains/phpstorm-attributes": "^1.0", 45 | "phpmd/phpmd": "^2.13", 46 | "phpstan/phpstan": "^1.5", 47 | "phpunit/phpunit": "^10.5" 48 | }, 49 | "minimum-stability": "dev", 50 | "prefer-stable": true, 51 | "autoload": { 52 | "psr-4": { 53 | "Framework\\Autoload\\": "src/" 54 | } 55 | }, 56 | "autoload-dev": { 57 | "psr-4": { 58 | "Tests\\Autoload\\": "tests/" 59 | } 60 | }, 61 | "config": { 62 | "allow-plugins": { 63 | "ergebnis/composer-normalize": true 64 | }, 65 | "optimize-autoloader": true, 66 | "preferred-install": "dist", 67 | "sort-packages": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Preloader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Autoload; 11 | 12 | use InvalidArgumentException; 13 | 14 | require_once __DIR__ . '/Autoloader.php'; 15 | require_once __DIR__ . '/Locator.php'; 16 | 17 | /** 18 | * Class Preloader. 19 | * 20 | * @see https://www.php.net/manual/en/opcache.preloading.php 21 | * 22 | * @package autoload 23 | */ 24 | class Preloader 25 | { 26 | /** 27 | * The main 'aplus' packages directory. 28 | * 29 | * @var string 30 | */ 31 | protected string $packagesDir; 32 | protected bool $loadPackages = true; 33 | protected bool $loadDevPackages = false; 34 | /** 35 | * The Autoloader instance necessary to autoload required classes. 36 | * 37 | * @var Autoloader 38 | */ 39 | protected Autoloader $autoloader; 40 | /** 41 | * The Locator instance used to list files. 42 | * 43 | * @var Locator 44 | */ 45 | protected Locator $locator; 46 | 47 | /** 48 | * Preloader constructor. 49 | * 50 | * @param Autoloader|null $autoloader A custom Autoloader instance or null 51 | * to auto initialize a new 52 | * @param string|null $packagesDir The main 'aplus' packages directory or 53 | * null to disable packages loading 54 | */ 55 | public function __construct( 56 | ?Autoloader $autoloader = null, 57 | ?string $packagesDir = __DIR__ . '/../../' 58 | ) { 59 | $this->loadPackages = $packagesDir !== null; 60 | if ($this->loadPackages) { 61 | $this->setPackagesDir($packagesDir); 62 | } 63 | $this->autoloader = $autoloader ?? new Autoloader(); 64 | $this->locator = new Locator($this->autoloader); 65 | } 66 | 67 | public function getAutoloader() : Autoloader 68 | { 69 | return $this->autoloader; 70 | } 71 | 72 | public function getLocator() : Locator 73 | { 74 | return $this->locator; 75 | } 76 | 77 | public function setPackagesDir(string $packagesDir) : static 78 | { 79 | $realpath = \realpath($packagesDir); 80 | if (!$realpath || !\is_dir($packagesDir)) { 81 | throw new InvalidArgumentException('Invalid packages dir: ' . $packagesDir); 82 | } 83 | $this->packagesDir = $realpath . \DIRECTORY_SEPARATOR; 84 | return $this; 85 | } 86 | 87 | public function getPackagesDir() : string 88 | { 89 | return $this->packagesDir; 90 | } 91 | 92 | public function withPackages() : static 93 | { 94 | $this->loadPackages = true; 95 | return $this; 96 | } 97 | 98 | public function withDevPackages() : static 99 | { 100 | $this->loadDevPackages = true; 101 | return $this; 102 | } 103 | 104 | /** 105 | * @param bool $setClasses 106 | * 107 | * @return array 108 | */ 109 | public function listPackagesFiles(bool $setClasses = true) : array 110 | { 111 | $result = []; 112 | foreach ($this->getLocator()->listFiles($this->getPackagesDir()) as $file) { 113 | if (!\str_ends_with($file, '.php')) { 114 | continue; 115 | } 116 | $className = $this->getLocator()->getClassName($file); 117 | if (!$className 118 | || ($className !== 'Aplus' && !\str_starts_with($className, 'Framework\\')) 119 | ) { 120 | continue; 121 | } 122 | if (!$this->loadDevPackages && $this->isDevelopmentClass($className)) { 123 | continue; 124 | } 125 | if ($setClasses) { 126 | $this->getAutoloader()->setClass($className, $file); 127 | } 128 | $result[] = $file; 129 | } 130 | \sort($result); 131 | return \array_unique($result); 132 | } 133 | 134 | protected function isDevelopmentClass(string $className) : bool 135 | { 136 | return \str_starts_with($className, 'Framework\CodingStandard\\') 137 | || \str_starts_with($className, 'Framework\Testing\\'); 138 | } 139 | 140 | /** 141 | * @param bool $setClasses 142 | * 143 | * @return array 144 | */ 145 | public function listFiles(bool $setClasses = true) : array 146 | { 147 | $result = []; 148 | foreach ($this->getAutoloader()->getClasses() as $file) { 149 | $result[] = $file; 150 | } 151 | foreach ($this->getAutoloader()->getNamespaces() as $namespace => $directories) { 152 | foreach ($directories as $directory) { 153 | $files = $this->getLocator()->listFiles($directory); 154 | foreach ($files as $file) { 155 | if (!\str_ends_with($file, '.php')) { 156 | continue; 157 | } 158 | $className = $this->getLocator()->getClassName($file); 159 | if (!$className 160 | || !\str_starts_with($className, $namespace . '\\') 161 | ) { 162 | continue; 163 | } 164 | if ($setClasses) { 165 | $this->getAutoloader()->setClass($className, $file); 166 | } 167 | $result[] = $file; 168 | } 169 | } 170 | } 171 | if ($this->loadPackages) { 172 | $result = [...$result, ...$this->listPackagesFiles($setClasses)]; 173 | } 174 | \sort($result); 175 | return \array_unique($result); 176 | } 177 | 178 | /** 179 | * Load files to be seen by the PHP OPcache Preloading when the engine starts. 180 | * 181 | * @return array The loaded files 182 | */ 183 | public function load() : array 184 | { 185 | $loadedFiles = []; 186 | foreach ($this->listFiles() as $file) { 187 | (static function () use ($file) : void { 188 | require_once $file; 189 | })(); 190 | $loadedFiles[] = $file; 191 | } 192 | return $loadedFiles; 193 | } 194 | 195 | /** 196 | * Get a list of all declared classes, interfaces and traits. 197 | * 198 | * @return array 199 | */ 200 | public static function getAllDeclarations() : array 201 | { 202 | $declarations = [ 203 | ...\get_declared_classes(), 204 | ...\get_declared_interfaces(), 205 | ...\get_declared_traits(), 206 | ]; 207 | \sort($declarations); 208 | return $declarations; 209 | } 210 | 211 | /** 212 | * Get a list of Framework declarations. 213 | * 214 | * @return array 215 | */ 216 | public static function getDeclarations() : array 217 | { 218 | $result = []; 219 | foreach (static::getAllDeclarations() as $declaration) { 220 | if ($declaration === 'Aplus' || \str_starts_with($declaration, 'Framework\\')) { 221 | $result[] = $declaration; 222 | } 223 | } 224 | return $result; 225 | } 226 | 227 | /** 228 | * Get a list of all included/required files. 229 | * 230 | * @return array 231 | */ 232 | public static function getIncludedFiles() : array 233 | { 234 | return \get_included_files(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Locator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Autoload; 11 | 12 | use JetBrains\PhpStorm\Pure; 13 | 14 | /** 15 | * Class Locator. 16 | * 17 | * The Locator class has methods for finding files and the class FQN using an 18 | * Autoloader instance. 19 | * 20 | * @package autoload 21 | */ 22 | class Locator 23 | { 24 | protected Autoloader $autoloader; 25 | 26 | /** 27 | * Locator constructor. 28 | * 29 | * @param Autoloader $autoloader 30 | */ 31 | public function __construct(Autoloader $autoloader) 32 | { 33 | $this->autoloader = $autoloader; 34 | } 35 | 36 | /** 37 | * Gets the first Qualified Class Name in a given filename. 38 | * 39 | * The "class name" can be the name of a class, an interface or a trait. 40 | * 41 | * @param string $filename 42 | * 43 | * @see https://www.php.net/manual/en/language.namespaces.rules.php 44 | * @see https://www.php.net/manual/en/language.oop5.interfaces.php 45 | * 46 | * @return string|null The class name or null if not found 47 | */ 48 | public function getClassName(string $filename) : ?string 49 | { 50 | if (!\is_file($filename)) { 51 | return null; 52 | } 53 | $tokens = \token_get_all((string) \file_get_contents($filename)); 54 | $last = \count($tokens); 55 | $namespace = ''; 56 | $class = ''; 57 | foreach ($tokens as $current => $token) { 58 | if ($token[0] === \T_NAMESPACE) { 59 | for ($next = $current + 1; $next < $last; $next++) { 60 | if ($tokens[$next][0] === \T_STRING || $tokens[$next][0] === \T_NAME_QUALIFIED) { 61 | $namespace .= '\\' . $tokens[$next][1]; 62 | } elseif ($tokens[$next] === '{' || $tokens[$next] === ';') { 63 | break; 64 | } 65 | } 66 | continue; 67 | } 68 | if ($token[0] === \T_RETURN) { 69 | return null; 70 | } 71 | if (\in_array($token[0], [ 72 | \T_CLASS, 73 | \T_ENUM, 74 | \T_INTERFACE, 75 | \T_TRAIT, 76 | ], true)) { 77 | for ($next = $current + 1; $next < $last; $next++) { 78 | if ($tokens[$next] === '{') { 79 | $token = $tokens[$current + 2]; 80 | $class = $namespace . '\\' . $token[1]; 81 | break 2; 82 | } 83 | } 84 | } 85 | } 86 | return $class ? \ltrim($class, '\\') : null; 87 | } 88 | 89 | /** 90 | * Get the first filepath found in all namespaces. 91 | * 92 | * @param string $file The file name without extension 93 | * @param string $extension The file extension 94 | * 95 | * @return string|null The filepath or null if not found 96 | */ 97 | public function getNamespacedFilepath(string $file, string $extension = '.php') : ?string 98 | { 99 | if ($extension) { 100 | $file = $this->ensureExtension($file, $extension); 101 | } 102 | $file = \strtr(\ltrim($file, '/'), ['\\' => '/']); 103 | $segments = \explode('/', $file); 104 | $count = \count($segments) - 1; 105 | $file = $segments[$count]; 106 | unset($segments[$count]); 107 | $namespaces = $this->autoloader->getNamespaces(); 108 | $namespace = ''; 109 | while ($segments) { 110 | $namespace .= $namespace === '' 111 | ? \array_shift($segments) 112 | : '\\' . \array_shift($segments); 113 | if (!isset($namespaces[$namespace])) { 114 | continue; 115 | } 116 | foreach ($namespaces[$namespace] as $directory) { 117 | $filepath = \rtrim( 118 | $directory . \implode(\DIRECTORY_SEPARATOR, $segments), 119 | \DIRECTORY_SEPARATOR 120 | ) . \DIRECTORY_SEPARATOR . $file; 121 | if (\is_file($filepath)) { 122 | return $filepath; 123 | } 124 | } 125 | break; 126 | } 127 | return \is_file($file) ? $file : null; 128 | } 129 | 130 | #[Pure] 131 | protected function ensureExtension(string $filename, string $extension) : string 132 | { 133 | if (!\str_ends_with($filename, $extension)) { 134 | $filename .= $extension; 135 | } 136 | return $filename; 137 | } 138 | 139 | /** 140 | * Find namesake files inside namespaced directories. 141 | * 142 | * @param string $filename The file name 143 | * @param string $extension The file extension 144 | * 145 | * @return array An array of filenames found 146 | */ 147 | #[Pure] 148 | public function findFiles(string $filename, string $extension = '.php') : array 149 | { 150 | if ($extension) { 151 | $filename = $this->ensureExtension($filename, $extension); 152 | } 153 | $files = []; 154 | foreach ($this->autoloader->getNamespaces() as $directories) { 155 | foreach ($directories as $directory) { 156 | if (\is_file($directory .= $filename)) { 157 | $files[] = $directory; 158 | } 159 | } 160 | } 161 | return $files; 162 | } 163 | 164 | /** 165 | * Get a list of all files inside namespaced sub directories. 166 | * 167 | * @param string $subDirectory Sub directory path 168 | * 169 | * @return array 170 | */ 171 | public function getFiles(string $subDirectory) : array 172 | { 173 | $namespacedFiles = []; 174 | foreach ($this->autoloader->getNamespaces() as $directories) { 175 | foreach ($directories as $directory) { 176 | $files = $this->listFiles($directory . $subDirectory); 177 | if ($files) { 178 | $namespacedFiles[] = $files; 179 | } 180 | } 181 | } 182 | return $namespacedFiles ? \array_merge(...$namespacedFiles) : []; 183 | } 184 | 185 | /** 186 | * Get a list of all files inside a directory. 187 | * 188 | * @param string $directory Absolute directory path 189 | * 190 | * @return array|null Returns an array of filenames or null 191 | * if the directory can not be resolved 192 | */ 193 | public function listFiles(string $directory) : ?array 194 | { 195 | $directory = \realpath($directory); 196 | if ($directory === false) { 197 | return null; 198 | } 199 | $directory .= \DIRECTORY_SEPARATOR; 200 | $files = []; 201 | foreach ((array) \scandir($directory, 0) as $filename) { 202 | if ($filename === '.' || $filename === '..') { 203 | continue; 204 | } 205 | $filename = $directory . $filename; 206 | if (\is_file($filename)) { 207 | $files[] = $filename; 208 | continue; 209 | } 210 | foreach ($this->listFiles($filename) as $subDirectory) { 211 | $files[] = $subDirectory; 212 | } 213 | } 214 | return $files; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Debug/AutoloadCollector.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Autoload\Debug; 11 | 12 | use Framework\Autoload\Autoloader; 13 | use Framework\Autoload\Preloader; 14 | use Framework\Debug\Collector; 15 | use Framework\Debug\Debugger; 16 | use UnitEnum; 17 | 18 | /** 19 | * Class AutoloadCollector. 20 | * 21 | * @package autoload 22 | */ 23 | class AutoloadCollector extends Collector 24 | { 25 | protected Autoloader $autoloader; 26 | 27 | public function setAutoloader(Autoloader $autoloader) : static 28 | { 29 | $this->autoloader = $autoloader; 30 | return $this; 31 | } 32 | 33 | public function getActivities() : array 34 | { 35 | $activities = []; 36 | foreach ($this->getData() as $data) { 37 | if ($data['loaded']) { 38 | $activities[] = [ 39 | 'collector' => $this->getName(), 40 | 'class' => static::class, 41 | 'description' => 'Load class ' . $data['class'], 42 | 'start' => $data['start'], 43 | 'end' => $data['end'], 44 | ]; 45 | } 46 | } 47 | return $activities; 48 | } 49 | 50 | public function getContents() : string 51 | { 52 | \ob_start(); ?> 53 |

Autoloader

54 | renderAutoloader() ?> 55 |

Included Files

56 | 58 |

Total of included files.

59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | $file): ?> 69 | getDataByFile($file); ?> 71 | > 74 | 75 | 76 | 77 | 78 | 79 | 80 |
#FileTime
81 |

Preload

82 | renderPreload() ?> 83 |

Declarations

84 | getPreloadStatistics()['classes'] ?? []; 87 | ?> 88 |

Total of declarations. 89 | 90 | are preloaded. 91 | 92 |

93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 107 | $declaration): ?> 108 | getDataByDeclaration($declaration); ?> 110 | > 112 | 113 | 114 | > 120 | 121 | 122 | 126 | 127 | 128 | 129 | 130 |
#TypeDeclarationLoadedTime
getDeclarationType($declaration) ?>
131 | |false 139 | */ 140 | protected function getDataByFile(string $file) : array | false 141 | { 142 | foreach ($this->getData() as $data) { 143 | if ($data['file'] === $file) { 144 | return $data; 145 | } 146 | } 147 | return false; 148 | } 149 | 150 | /** 151 | * @param string $declaration 152 | * 153 | * @return array|false 154 | */ 155 | protected function getDataByDeclaration(string $declaration) : array | false 156 | { 157 | foreach ($this->getData() as $data) { 158 | if ($data['class'] === $declaration) { 159 | return $data; 160 | } 161 | } 162 | return false; 163 | } 164 | 165 | protected function renderAutoloader() : string 166 | { 167 | if (!isset($this->autoloader)) { 168 | return '

An Autoloader instance has not been set on this collector.

'; 169 | } 170 | \ob_start(); ?> 171 |

Namespaces

172 | renderNamespaces() ?> 173 |

Classes

174 | renderClasses(); 176 | return \ob_get_clean(); // @phpstan-ignore-line 177 | } 178 | 179 | protected function renderNamespaces() : string 180 | { 181 | $namespaces = $this->autoloader->getNamespaces(); 182 | if (empty($namespaces)) { 183 | return '

No namespace directory has been set on this Autoloader instance.

'; 184 | } 185 | \ksort($namespaces); 186 | \ob_start(); ?> 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | $directories): ?> 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 |
NamespaceDirectories
209 | autoloader->getClasses(); 216 | if (empty($classes)) { 217 | return '

No class file has been set on this Autoloader instance.

'; 218 | } 219 | \ksort($classes); 220 | \ob_start(); ?> 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | $file): ?> 230 | 231 | 232 | 233 | 234 | 235 | 236 |
ClassFile
237 | getOpcacheConfiguration(); 244 | if ($conf === null) { 245 | return '

Preload is not available.

'; 246 | } 247 | if ($conf && !empty($conf['directives']['opcache.preload'])) { 248 | $result = '

File: ' 249 | . \htmlentities($conf['directives']['opcache.preload']) . '

'; 250 | if (!empty($conf['directives']['opcache.preload_user'])) { 251 | $result .= '

User: ' 252 | . \htmlentities($conf['directives']['opcache.preload_user']) . '

'; 253 | } 254 | $result .= $this->renderPreloadStatistics(); 255 | return $result; 256 | } 257 | return '

Preload file has not been set.

'; 258 | } 259 | 260 | /** 261 | * @return array|null 262 | */ 263 | protected function getOpcacheConfiguration() : ?array 264 | { 265 | return \function_exists('opcache_get_configuration') 266 | ? (array) \opcache_get_configuration() 267 | : null; 268 | } 269 | 270 | protected function renderPreloadStatistics() : string 271 | { 272 | $result = ''; 273 | $statistics = $this->getPreloadStatistics(); 274 | if ($statistics) { 275 | $memory = Debugger::convertSize($statistics['memory_consumption']); 276 | $classes = $statistics['classes']; 277 | $result .= '

Memory: ' . $memory . '

'; 278 | $result .= '

Classes: ' . \count($classes) . '

'; 279 | } 280 | return $result; 281 | } 282 | 283 | /** 284 | * @return array 285 | */ 286 | protected function getPreloadStatistics() : array 287 | { 288 | $statistics = []; 289 | if (\function_exists('opcache_get_status')) { 290 | $status = (array) \opcache_get_status(); 291 | $statistics = $status['preload_statistics'] ?? []; 292 | } 293 | if (empty($statistics) && \defined('IS_TESTING') && IS_TESTING) { 294 | $statistics = [ 295 | 'memory_consumption' => 500000, 296 | 'classes' => [ 297 | 'Foo', 298 | 'Bar', 299 | ], 300 | ]; 301 | } 302 | return $statistics; 303 | } 304 | 305 | protected function getDeclarationType(string $declaration) : string 306 | { 307 | if (\in_array($declaration, \get_declared_classes(), true)) { 308 | return \is_subclass_of($declaration, UnitEnum::class) ? 'enum' : 'class'; 309 | } 310 | if (\in_array($declaration, \get_declared_interfaces(), true)) { 311 | return 'interface'; 312 | } 313 | if (\in_array($declaration, \get_declared_traits(), true)) { 314 | return 'trait'; 315 | } 316 | return ''; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/Autoloader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\Autoload; 11 | 12 | use Framework\Autoload\Debug\AutoloadCollector; 13 | use Framework\Debug\Collector; 14 | use JetBrains\PhpStorm\Pure; 15 | use RuntimeException; 16 | 17 | /** 18 | * Class Autoloader. 19 | * 20 | * The Autoloader class allows to set namespace directories to search for files 21 | * (PSR4) and set the absolute path of classes without namespaces (PSR0). 22 | * 23 | * @package autoload 24 | */ 25 | class Autoloader 26 | { 27 | /** 28 | * List of classes to file paths. 29 | * 30 | * @var array 31 | */ 32 | protected array $classes = []; 33 | /** 34 | * List of namespaces to directory paths. 35 | * 36 | * @var array> 37 | */ 38 | protected array $namespaces = []; 39 | protected AutoloadCollector $debugCollector; 40 | 41 | /** 42 | * Autoloader constructor. 43 | * 44 | * @param bool $register Register the Autoloader::loadClass() as autoload 45 | * implementation 46 | * @param string $extensions A comma delimited list of file extensions for 47 | * spl_autoload 48 | * 49 | * @see Autoloader::loadClass() 50 | */ 51 | public function __construct(bool $register = true, string $extensions = '.php') 52 | { 53 | if ($register) { 54 | $this->register($extensions); 55 | } 56 | } 57 | 58 | /** 59 | * Registers the Autoloader::loadClass() as autoload implementation. 60 | * 61 | * @param string $extensions A comma delimited list of file extensions for 62 | * spl_autoload 63 | * 64 | * @see Autoloader::loadClass() 65 | * 66 | * @return bool True on success or false on failure 67 | */ 68 | public function register(string $extensions = '.php') : bool 69 | { 70 | \spl_autoload_extensions($extensions); 71 | // @phpstan-ignore-next-line 72 | return \spl_autoload_register([$this, 'loadClass'], true, false); 73 | } 74 | 75 | /** 76 | * Unregisters the Autoloader::loadClass() as autoload implementation. 77 | * 78 | * @see Autoloader::loadClass() 79 | * 80 | * @return bool True on success or false on failure 81 | */ 82 | public function unregister() : bool 83 | { 84 | return \spl_autoload_unregister([$this, 'loadClass']); 85 | } 86 | 87 | /** 88 | * Sets one namespace mapping for a directory path. 89 | * 90 | * @param string $namespace Namespace name 91 | * @param array|string $dir Directory path 92 | * 93 | * @return static 94 | */ 95 | public function setNamespace(string $namespace, array | string $dir) : static 96 | { 97 | $directories = $this->makeRenderedDirectoryPaths((array) $dir); 98 | if ($directories) { 99 | $this->namespaces[$this->renderRealName($namespace)] = $directories; 100 | $this->sortNamespaces(); 101 | } 102 | return $this; 103 | } 104 | 105 | /** 106 | * @param array $directories 107 | * 108 | * @return array 109 | */ 110 | protected function makeRenderedDirectoryPaths(array $directories) : array 111 | { 112 | $paths = []; 113 | foreach ($directories as $directory) { 114 | $paths[] = $this->renderDirectoryPath($directory); 115 | } 116 | return $paths; 117 | } 118 | 119 | /** 120 | * Adds directory paths to a namespace. 121 | * 122 | * @param string $namespace Namespace name 123 | * @param array|string $dir Directory path 124 | * 125 | * @return static 126 | */ 127 | public function addNamespace(string $namespace, array | string $dir) : static 128 | { 129 | $directories = $this->makeRenderedDirectoryPaths((array) $dir); 130 | if ($directories) { 131 | $name = $this->renderRealName($namespace); 132 | if (isset($this->namespaces[$name])) { 133 | $directories = [...$this->namespaces[$name], ...$directories]; 134 | } 135 | $this->namespaces[$name] = $directories; 136 | $this->sortNamespaces(); 137 | } 138 | return $this; 139 | } 140 | 141 | protected function sortNamespaces() : void 142 | { 143 | \krsort($this->namespaces); 144 | } 145 | 146 | /** 147 | * Sets namespaces mapping for directory paths. 148 | * 149 | * @param array|string> $namespaces Namespace names 150 | * as keys and directory paths as values 151 | * 152 | * @return static 153 | */ 154 | public function setNamespaces(array $namespaces) : static 155 | { 156 | foreach ($namespaces as $name => $dir) { 157 | $this->setNamespace($name, $dir); 158 | } 159 | $this->sortNamespaces(); 160 | return $this; 161 | } 162 | 163 | /** 164 | * Adds directory paths to namespaces. 165 | * 166 | * @param array|string> $namespaces Namespace names 167 | * as keys and directory paths as values 168 | * 169 | * @return static 170 | */ 171 | public function addNamespaces(array $namespaces) : static 172 | { 173 | foreach ($namespaces as $name => $dir) { 174 | $this->addNamespace($name, $dir); 175 | } 176 | return $this; 177 | } 178 | 179 | /** 180 | * Gets the directory paths for a given namespace. 181 | * 182 | * @param string $name Namespace name 183 | * 184 | * @return array The namespace directory paths 185 | */ 186 | #[Pure] 187 | public function getNamespace(string $name) : array 188 | { 189 | return $this->namespaces[$this->renderRealName($name)] ?? []; 190 | } 191 | 192 | /** 193 | * Gets all mapped namespaces. 194 | * 195 | * @return array> 196 | */ 197 | #[Pure] 198 | public function getNamespaces() : array 199 | { 200 | return $this->namespaces; 201 | } 202 | 203 | /** 204 | * Removes one namespace from the mapping. 205 | * 206 | * @param string $name Namespace name 207 | * 208 | * @return static 209 | */ 210 | public function removeNamespace(string $name) : static 211 | { 212 | unset($this->namespaces[$this->renderRealName($name)]); 213 | return $this; 214 | } 215 | 216 | /** 217 | * Removes namespaces from the mapping. 218 | * 219 | * @param array $names List of namespace names 220 | * 221 | * @return static 222 | */ 223 | public function removeNamespaces(array $names) : static 224 | { 225 | foreach ($names as $name) { 226 | $this->removeNamespace($name); 227 | } 228 | return $this; 229 | } 230 | 231 | /** 232 | * Sets one class mapping for a file path. 233 | * 234 | * @param string $name Fully qualified class name (with namespace) 235 | * @param string $filepath Class file path 236 | * 237 | * @return static 238 | */ 239 | public function setClass(string $name, string $filepath) : static 240 | { 241 | $this->classes[$this->renderRealName($name)] = $this->renderFilePath($filepath); 242 | return $this; 243 | } 244 | 245 | /** 246 | * Sets classes mapping for file paths. 247 | * 248 | * @param array $classes Associative array with class names 249 | * as keys and file paths as values 250 | * 251 | * @return static 252 | */ 253 | public function setClasses(array $classes) : static 254 | { 255 | foreach ($classes as $name => $filepath) { 256 | $this->setClass($name, $filepath); 257 | } 258 | return $this; 259 | } 260 | 261 | /** 262 | * Gets a class file path. 263 | * 264 | * @param string $name Fully qualified class name (with namespace) 265 | * 266 | * @return string|null The file path or null if class is not mapped 267 | */ 268 | #[Pure] 269 | public function getClass(string $name) : ?string 270 | { 271 | return $this->classes[$this->renderRealName($name)] ?? null; 272 | } 273 | 274 | /** 275 | * Gets all mapped classes. 276 | * 277 | * @return array An array of class names as keys and 278 | * file paths as values 279 | */ 280 | #[Pure] 281 | public function getClasses() : array 282 | { 283 | return $this->classes; 284 | } 285 | 286 | /** 287 | * Removes one class from the mapping. 288 | * 289 | * @param string $name Fully qualified class name (with namespace) 290 | * 291 | * @return static 292 | */ 293 | public function removeClass(string $name) : static 294 | { 295 | unset($this->classes[$this->renderRealName($name)]); 296 | return $this; 297 | } 298 | 299 | /** 300 | * Removes classes from the mapping. 301 | * 302 | * @param array $names List of class names 303 | * 304 | * @return static 305 | */ 306 | public function removeClasses(array $names) : static 307 | { 308 | foreach ($names as $name) { 309 | $this->removeClass($name); 310 | } 311 | return $this; 312 | } 313 | 314 | /** 315 | * Finds the file path of a class searching in the class mapping and 316 | * resolving namespaces. 317 | * 318 | * @param string $class Fully qualified class name (with namespace) 319 | * 320 | * @return string|null The class file path or null if not found 321 | */ 322 | #[Pure] 323 | public function findClassPath(string $class) : ?string 324 | { 325 | $path = $this->getClass($class); 326 | if ($path) { 327 | return $path; 328 | } 329 | foreach ($this->getNamespaces() as $namespace => $paths) { 330 | $namespace .= '\\'; 331 | if (\str_starts_with($class, $namespace)) { 332 | foreach ($paths as $path) { 333 | $path .= \strtr( 334 | \substr($class, \strlen($namespace)), 335 | ['\\' => \DIRECTORY_SEPARATOR] 336 | ); 337 | $path .= '.php'; 338 | if (\is_file($path)) { 339 | return $path; 340 | } 341 | } 342 | } 343 | } 344 | return null; 345 | } 346 | 347 | /** 348 | * Loads a class file. 349 | * 350 | * @param string $class Fully qualified class name (with namespace) 351 | * 352 | * @return bool TRUE if the file is loaded, otherwise FALSE 353 | */ 354 | public function loadClass(string $class) : bool 355 | { 356 | if (isset($this->debugCollector)) { 357 | return $this->loadDebug($class); 358 | } 359 | return $this->loadClassFile($class); 360 | } 361 | 362 | protected function loadClassFile(string $class) : bool 363 | { 364 | $class = $this->findClassPath($class); 365 | if ($class) { 366 | // Require $class in an isolated scope - no access to $this 367 | (static function () use ($class) : void { 368 | require $class; 369 | })(); 370 | return true; 371 | } 372 | return false; 373 | } 374 | 375 | protected function loadDebug(string $class) : bool 376 | { 377 | $start = \microtime(true); 378 | $loaded = $this->loadClassFile($class); 379 | $end = \microtime(true); 380 | $this->debugCollector->addData([ 381 | 'start' => $start, 382 | 'end' => $end, 383 | 'class' => $class, 384 | 'file' => $this->findClassPath($class), 385 | 'loaded' => $loaded, 386 | ]); 387 | return $loaded; 388 | } 389 | 390 | public function setDebugCollector(?AutoloadCollector $debugCollector = null, string $name = 'default') : static 391 | { 392 | if ($debugCollector) { 393 | $this->debugCollector = $debugCollector; 394 | return $this; 395 | } 396 | $data = []; 397 | foreach ([Collector::class, AutoloadCollector::class] as $class) { 398 | $start = \microtime(true); 399 | $loaded = $this->loadClassFile($class); 400 | $end = \microtime(true); 401 | $data[] = [ 402 | 'start' => $start, 403 | 'end' => $end, 404 | 'class' => $class, 405 | 'file' => $this->findClassPath($class), 406 | 'loaded' => $loaded, 407 | ]; 408 | } 409 | $this->debugCollector = new AutoloadCollector($name); 410 | $this->debugCollector->setAutoloader($this); 411 | foreach ($data as $item) { 412 | $this->debugCollector->addData($item); 413 | } 414 | return $this; 415 | } 416 | 417 | public function getDebugCollector() : ?AutoloadCollector 418 | { 419 | return $this->debugCollector ?? null; 420 | } 421 | 422 | /** 423 | * Renders a class or namespace name without lateral slashes. 424 | * 425 | * @param string $name Class or namespace name 426 | * 427 | * @return string 428 | */ 429 | #[Pure] 430 | protected function renderRealName(string $name) : string 431 | { 432 | return \trim($name, '\\'); 433 | } 434 | 435 | /** 436 | * Renders the canonicalized absolute pathname for a file path. 437 | * 438 | * @param string $path File path 439 | * 440 | * @return string 441 | */ 442 | protected function renderFilePath(string $path) : string 443 | { 444 | $real = \realpath($path); 445 | if ($real === false || !\is_file($real)) { 446 | throw new RuntimeException("Path is not a file: {$path}"); 447 | } 448 | return $real; 449 | } 450 | 451 | /** 452 | * Gets the canonicalized absolute pathname for a directory path. 453 | * 454 | * Adds a trailing slash. 455 | * 456 | * @param string $path 457 | * 458 | * @return string 459 | */ 460 | protected function renderDirectoryPath(string $path) : string 461 | { 462 | $real = \realpath($path); 463 | if ($real === false || !\is_dir($real)) { 464 | throw new RuntimeException("Path is not a directory: {$path}"); 465 | } 466 | return $real . \DIRECTORY_SEPARATOR; 467 | } 468 | } 469 | --------------------------------------------------------------------------------