Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.40% covered (success)
91.40%
85 / 93
75.00% covered (warning)
75.00%
9 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
AutoLoader
91.40% covered (success)
91.40%
85 / 93
75.00% covered (warning)
75.00%
9 / 12
43.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
4
 adjustNamespacePrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 adjustDirectory
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 adjustClassName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setImpl
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
16
 set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 register
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 unregister
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findIncludeFile
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 load
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core;
6
7use 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 */
21class 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}