Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.40% |
85 / 93 |
|
75.00% |
9 / 12 |
CRAP | |
0.00% |
0 / 1 |
AutoLoader | |
91.40% |
85 / 93 |
|
75.00% |
9 / 12 |
43.12 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
4 | |||
adjustNamespacePrefix | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
adjustDirectory | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
adjustClassName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setImpl | |
100.00% |
35 / 35 |
|
100.00% |
1 / 1 |
16 | |||
set | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
add | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
get | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
register | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
unregister | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findIncludeFile | |
78.57% |
11 / 14 |
|
0.00% |
0 / 1 |
7.48 | |||
load | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core; |
6 | |
7 | use Error; |
8 | |
9 | /** |
10 | * オートローダー。 |
11 | * |
12 | * NOTE: なにがあってもPHP標準関数ですべて処理すること。 |
13 | * 調整する場合は継承してお好きに。 |
14 | * |
15 | * @phpstan-type NamespacePrefixAlias string |
16 | * @phpstan-type BaseDirectoryAlias non-empty-string |
17 | * @phpstan-type ClassIncludesAlias array<class-string,class-string|non-empty-string> |
18 | * @phpstan-type InputMappingIncludesAlias array{directory:BaseDirectoryAlias,includes?:ClassIncludesAlias|null,extensions?:array<non-empty-string>|null} |
19 | * @phpstan-type EnabledMappingIncludesAlias array{directory:BaseDirectoryAlias,includes:ClassIncludesAlias,extensions:non-empty-array<non-empty-string>} |
20 | */ |
21 | class AutoLoader |
22 | { |
23 | #region variable |
24 | |
25 | /** |
26 | * 名前空間接頭辞とプロジェクトマッピング。 |
27 | * |
28 | * @var array |
29 | * @phpstan-var array<NamespacePrefixAlias,EnabledMappingIncludesAlias> |
30 | */ |
31 | private $setting = []; |
32 | |
33 | #endregion |
34 | |
35 | /** |
36 | * 生成。 |
37 | * |
38 | * @param array|null $setting 初期化設定。内部的には `add` が実施される。 |
39 | * @phpstan-param array<NamespacePrefixAlias,InputMappingIncludesAlias>|null $setting |
40 | */ |
41 | public function __construct(?array $setting = null) |
42 | { |
43 | if ($setting !== null) { |
44 | foreach ($setting as $k => $v) { |
45 | $this->setImpl($k, $v, false); |
46 | } |
47 | } |
48 | |
49 | // Core 固有ライブラリ登録 |
50 | $libs = __DIR__ . DIRECTORY_SEPARATOR . 'Libs' . DIRECTORY_SEPARATOR; |
51 | /** @phpstan-var array<NamespacePrefixAlias,InputMappingIncludesAlias> */ |
52 | $libraries = [ |
53 | // highlight.php |
54 | 'Highlight' => [ |
55 | 'directory' => $libs . 'highlight.php/Highlight', |
56 | ], |
57 | // php-markdown |
58 | 'Michelf' => [ |
59 | 'directory' => $libs . 'php-markdown/Michelf', |
60 | ], |
61 | // PHPMailer |
62 | 'PHPMailer\PHPMailer' => [ |
63 | 'directory' => $libs . 'PHPMailer/src', |
64 | ], |
65 | // Smarty |
66 | 'Smarty' => [ |
67 | 'directory' => $libs . 'smarty/src', |
68 | ], |
69 | // Psr |
70 | 'Psr\Container' => [ |
71 | 'directory' => $libs . 'php-fig/container/src', |
72 | ], |
73 | ]; |
74 | foreach ($libraries as $k => $v) { |
75 | $this->setImpl($k, $v, false); |
76 | } |
77 | } |
78 | |
79 | #region function |
80 | |
81 | /** |
82 | * 名前空間接頭辞の調整。 |
83 | * |
84 | * @param string $namespacePrefix |
85 | * @phpstan-param NamespacePrefixAlias $namespacePrefix |
86 | * @return string |
87 | * @phpstan-return NamespacePrefixAlias |
88 | */ |
89 | protected function adjustNamespacePrefix(string $namespacePrefix): string |
90 | { |
91 | return trim($namespacePrefix, "\\ \t") . '\\'; |
92 | } |
93 | |
94 | /** |
95 | * 基底ディレクトリの調整。 |
96 | * |
97 | * @param string $path |
98 | * @phpstan-param BaseDirectoryAlias $path |
99 | * @return string |
100 | */ |
101 | protected function adjustDirectory(string $path): string |
102 | { |
103 | $trimPath = rtrim($path, "\\/ \t"); |
104 | $replacePath = str_replace('\\', DIRECTORY_SEPARATOR, $trimPath); |
105 | if (empty($replacePath) && $replacePath !== '0') { |
106 | return ''; |
107 | } |
108 | |
109 | return $replacePath . DIRECTORY_SEPARATOR; |
110 | } |
111 | |
112 | /** |
113 | * クラス名の調整。 |
114 | * |
115 | * @param class-string|string $className |
116 | * @return string |
117 | */ |
118 | protected function adjustClassName(string $className): string |
119 | { |
120 | return trim($className, "\\ \t"); |
121 | } |
122 | |
123 | /** |
124 | * マッピング設定。 |
125 | * |
126 | * @param string $namespacePrefix |
127 | * @phpstan-param NamespacePrefixAlias $namespacePrefix |
128 | * @param array $mapping |
129 | * @phpstan-param InputMappingIncludesAlias $mapping |
130 | * @param bool $overwrite |
131 | */ |
132 | protected function setImpl(string $namespacePrefix, array $mapping, bool $overwrite): void |
133 | { |
134 | $fixedNamespacePrefixAlias = $this->adjustNamespacePrefix($namespacePrefix); |
135 | if (!$overwrite && isset($this->setting[$fixedNamespacePrefixAlias])) { |
136 | throw new Error('[' . $namespacePrefix . '] overwrite: $namespacePrefix => ' . $fixedNamespacePrefixAlias); |
137 | } |
138 | |
139 | $fixedDirectoryPath = $this->adjustDirectory($mapping['directory']); |
140 | if (empty($fixedDirectoryPath)) { |
141 | throw new Error('[' . $namespacePrefix . '] empty: $mapping[\'directory\'] => ' . $fixedDirectoryPath); |
142 | } |
143 | |
144 | /** @phpstan-var ClassIncludesAlias */ |
145 | $fixedIncludes = []; |
146 | if (isset($mapping['includes']) && $mapping['includes'] !== null) { //@phpstan-ignore-line non-empty-array !== null |
147 | foreach ($mapping['includes'] as $aliasClassName => $includeClassName) { |
148 | $fixedAliasClassName = $this->adjustClassName($aliasClassName); |
149 | $fixedIncludeClassName = $this->adjustClassName($includeClassName); |
150 | |
151 | if (isset($fixedIncludes[$fixedAliasClassName])) { |
152 | throw new Error('[' . $namespacePrefix . '] overwrite: $aliasClassName => ' . $fixedAliasClassName); |
153 | } |
154 | if (empty($fixedAliasClassName)) { |
155 | throw new Error('[' . $namespacePrefix . '] empty: $aliasClassName => ' . $fixedAliasClassName); |
156 | } |
157 | if (empty($fixedIncludeClassName)) { |
158 | throw new Error('[' . $namespacePrefix . '] empty: $includeClassName => ' . $fixedIncludeClassName); |
159 | } |
160 | |
161 | $fixedIncludes[$fixedAliasClassName] = $fixedIncludeClassName; |
162 | } |
163 | } |
164 | |
165 | $fixedExtensions = []; |
166 | if (isset($mapping['extensions'])) { |
167 | foreach ($mapping['extensions'] as $extension) { |
168 | $extension = trim($extension); |
169 | if (!empty($extension)) { |
170 | if ($extension[0] !== '.') { |
171 | $extension = '.' . $extension; |
172 | } |
173 | if (!in_array($extension, $fixedExtensions, true)) { |
174 | $fixedExtensions[] = $extension; |
175 | } |
176 | } |
177 | } |
178 | } |
179 | if (!count($fixedExtensions)) { |
180 | $fixedExtensions = ['.php']; |
181 | } |
182 | |
183 | /** @phpstan-var EnabledMappingIncludesAlias */ |
184 | $enabledMapping = [ |
185 | 'directory' => $fixedDirectoryPath, |
186 | 'includes' => $fixedIncludes, |
187 | 'extensions' => $fixedExtensions, |
188 | ]; |
189 | $this->setting[$fixedNamespacePrefixAlias] = $enabledMapping; |
190 | } |
191 | |
192 | /** |
193 | * マッピング設定。 |
194 | * |
195 | * 既存のマッピングが存在する場合は上書き。 |
196 | * |
197 | * @param string $namespacePrefix |
198 | * @phpstan-param NamespacePrefixAlias $namespacePrefix |
199 | * @param array $mapping |
200 | * @phpstan-param InputMappingIncludesAlias $mapping |
201 | */ |
202 | final public function set(string $namespacePrefix, array $mapping): void |
203 | { |
204 | $this->setImpl($namespacePrefix, $mapping, true); |
205 | } |
206 | |
207 | /** |
208 | * マッピング追加。 |
209 | * |
210 | * @param string $namespacePrefix |
211 | * @phpstan-param NamespacePrefixAlias $namespacePrefix |
212 | * @param array $mapping |
213 | * @phpstan-param InputMappingIncludesAlias $mapping |
214 | * @throws Error 既存のマッピングが存在する |
215 | */ |
216 | final public function add(string $namespacePrefix, array $mapping): void |
217 | { |
218 | $this->setImpl($namespacePrefix, $mapping, false); |
219 | } |
220 | |
221 | /** |
222 | * マッピング取得。 |
223 | * |
224 | * @param string $namespacePrefix |
225 | * @return array<string,mixed>|null 見つからなかった場合は `null` を返す。 |
226 | * @phpstan-return EnabledMappingIncludesAlias|null |
227 | */ |
228 | final public function get(string $namespacePrefix): ?array |
229 | { |
230 | $fixedNamespacePrefix = $this->adjustNamespacePrefix($namespacePrefix); |
231 | |
232 | if (isset($this->setting[$fixedNamespacePrefix])) { |
233 | return $this->setting[$fixedNamespacePrefix]; |
234 | } |
235 | |
236 | return null; |
237 | } |
238 | |
239 | /** |
240 | * 登録。 |
241 | * |
242 | * `spl_autoload_register` ラッパー。 |
243 | * |
244 | * @param bool $prepend キューの先頭に登録するか。 |
245 | * @return void |
246 | * @throws Error 登録ディレクトが存在しない |
247 | * @see https://www.php.net/manual/function.spl-autoload-register.php |
248 | */ |
249 | public function register(bool $prepend = false): void |
250 | { |
251 | foreach ($this->setting as $namespacePrefix => $mapping) { |
252 | if (!is_dir($mapping['directory'])) { |
253 | throw new Error('[' . $namespacePrefix . '] not found: $mapping[\'directory\'] => ' . $mapping['directory']); |
254 | } |
255 | } |
256 | |
257 | spl_autoload_register([$this, 'load'], true, $prepend); |
258 | } |
259 | |
260 | /** |
261 | * 登録解除。 |
262 | * |
263 | * `spl_autoload_unregister` ラッパー。 |
264 | * |
265 | * @return bool 解除できたか。 |
266 | * @see https://www.php.net/manual/function.spl-autoload-unregister.php |
267 | */ |
268 | public function unregister(): bool |
269 | { |
270 | return spl_autoload_unregister([$this, 'load']); |
271 | } |
272 | |
273 | /** |
274 | * 読み込み対象ファイルの取得。 |
275 | * |
276 | * 本処理はエラーとか例外はやっちゃいけない。 |
277 | * |
278 | * @param string $fullName |
279 | * @return non-empty-string|null ファイルが存在する場合はファイルパス。存在しない(担当じゃない)場合は `null` を返す。 |
280 | */ |
281 | protected function findIncludeFile(string $fullName): ?string |
282 | { |
283 | foreach ($this->setting as $namespacePrefix => $mapping) { |
284 | if (str_starts_with($fullName, $namespacePrefix)) { |
285 | foreach ($mapping['includes'] as $aliasClassName => $includeClassName) { |
286 | if ($aliasClassName === $fullName) { |
287 | $fullName = $includeClassName; |
288 | break; |
289 | } |
290 | } |
291 | |
292 | $classBaseName = substr($fullName, strlen($namespacePrefix)); |
293 | $fileBasePath = str_replace('\\', DIRECTORY_SEPARATOR, $classBaseName); |
294 | $filePathWithoutExtensions = $mapping['directory'] . $fileBasePath; |
295 | |
296 | foreach ($mapping['extensions'] as $extension) { |
297 | $filePath = $filePathWithoutExtensions . $extension; |
298 | if (is_file($filePath)) { |
299 | return $filePath; |
300 | } |
301 | } |
302 | } |
303 | } |
304 | |
305 | return null; |
306 | } |
307 | |
308 | /** |
309 | * ファイル読み込み処理。 |
310 | * |
311 | * @param string $fullName 完全名。 |
312 | */ |
313 | private function load(string $fullName): void |
314 | { |
315 | $filePath = $this->findIncludeFile($fullName); |
316 | if ($filePath !== null) { |
317 | require $filePath; |
318 | } |
319 | } |
320 | |
321 | #endregion |
322 | } |