Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.22% covered (warning)
72.22%
39 / 54
46.67% covered (danger)
46.67%
7 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Encoding
72.22% covered (warning)
72.22%
39 / 54
46.67% covered (danger)
46.67%
7 / 15
42.63
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 throwIfInvalidEncodingName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getEncodingNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultEncoding
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 setDefaultEncoding
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getAscii
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUtf8
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUtf16
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUtf32
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getShiftJis
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBinary
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 toString
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
4.59
 getAliasNames
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 isValid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getByteOrderMark
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core;
6
7use ValueError;
8use PeServer\Core\Binary;
9use PeServer\Core\Collection\Arr;
10use PeServer\Core\Throws\ArgumentException;
11use PeServer\Core\Throws\EncodingException;
12use PeServer\Core\Throws\InvalidOperationException;
13use PeServer\Core\Throws\Throws;
14
15/**
16 * エンコーディング処理。
17 *
18 * コンストラクタで指定されたエンコード名に対して通常文字列へあれこれする。
19 */
20class Encoding
21{
22    #region define
23
24    /** アスキー */
25    public const ENCODE_ASCII = 'ASCII';
26
27    /** UTF-7 */
28    public const ENCODE_UTF7 = 'UTF-7';
29    /** UTF-8 */
30    public const ENCODE_UTF8 = 'UTF-8';
31    /** UTF-16 */
32    public const ENCODE_UTF16_PLAIN = 'UTF-16';
33    public const ENCODE_UTF16_BE = 'UTF-16BE';
34    public const ENCODE_UTF16_LE = 'UTF-16LE';
35    public const ENCODE_UTF16_DEFAULT = self::ENCODE_UTF16_LE;
36    /** UTF-32 */
37    public const ENCODE_UTF32_PLAIN = 'UTF-32';
38    public const ENCODE_UTF32_BE = 'UTF-32BE';
39    public const ENCODE_UTF32_LE = 'UTF-32LE';
40    public const ENCODE_UTF32_DEFAULT = self::ENCODE_UTF32_LE;
41
42    /** SJIS(SHIFT-JIS) */
43    public const ENCODE_SJIS_PLAIN = 'SJIS';
44    /** SJIS(CP932) */
45    public const ENCODE_SJIS_WIN31J = 'CP932';
46    /** SJIS(Windows) */
47    public const ENCODE_SJIS_WIN = 'SJIS-win';
48    /** SJIS(Shift_JIS-2004) */
49    public const ENCODE_SJIS_2004 = 'SJIS-2004';
50    /** SJIS(何も考えず使う用) */
51    public const ENCODE_SJIS_DEFAULT = self::ENCODE_SJIS_WIN31J;
52
53    public const ENCODE_JIS_PLAIN = 'JIS';
54    public const ENCODE_JIS_DEFAULT = self::ENCODE_JIS_PLAIN;
55
56    public const ENCODE_EUC_JP_PLAIN = 'EUC-JP';
57    public const ENCODE_EUC_JP_WIN = 'eucJP-win';
58    public const ENCODE_EUC_JP_DEFAULT = self::ENCODE_EUC_JP_WIN;
59
60    #endregion
61
62    #region variable
63
64    /**
65     * キャッシュされたエンコーディング名一覧。
66     *
67     * @var string[]|null
68     */
69    protected static ?array $cacheNames = null;
70
71    /**
72     * デフォルトエンコーディング。
73     *
74     * `setDefaultEncoding` で設定され、
75     * `getDefaultEncoding` で使用される。
76     *
77     * ただし `getDefaultEncoding` で本プロパティ未設定の場合は上書きされる。
78     *
79     * @var self|null
80     */
81    private static ?self $defaultEncoding = null;
82
83    /**
84     * エンコード名。
85     */
86    public readonly string $name;
87
88    #endregion
89
90    /**
91     * 生成
92     *
93     * @param string $name エンコード名。
94     * @phpstan-param non-empty-string|Encoding::ENCODE_* $name
95     */
96    public function __construct(string $name)
97    {
98        self::throwIfInvalidEncodingName($name);
99        $this->name = $name;
100    }
101
102    #region function
103
104    /**
105     * エンコーディング名が正しいか。
106     *
107     * @param string $name
108     * @throws ArgumentException 正しくない。
109     */
110    private static function throwIfInvalidEncodingName(string $name): void
111    {
112        $names = self::getEncodingNames();
113        if (!Arr::containsValue($names, $name)) {
114            throw new ArgumentException('$name');
115        }
116    }
117
118    /**
119     * エンコーディング名一覧を取得。
120     *
121     * キャッシュされる。
122     *
123     * `mb_list_encodings` ラッパー。
124     *
125     * @return string[]
126     * @see https://www.php.net/manual/function.mb-list-encodings.php
127     */
128    public static function getEncodingNames(): array
129    {
130        return self::$cacheNames ??= mb_list_encodings();
131    }
132
133    /**
134     * `mb_internal_encoding` ラッパー
135     *
136     * @return Encoding
137     * @see https://www.php.net/manual/function.mb-internal-encoding.php
138     */
139    public static function getDefaultEncoding(): Encoding
140    {
141        if (self::$defaultEncoding === null) {
142            $name = mb_internal_encoding();
143            if (Text::isNullOrEmpty($name)) {
144                throw new InvalidOperationException();
145            }
146
147            return self::$defaultEncoding = new Encoding($name);
148        }
149
150        return self::$defaultEncoding;
151    }
152
153    /**
154     * `mb_internal_encoding` ラッパー
155     *
156     * @param Encoding $encoding
157     * @throws ArgumentException
158     * @see https://www.php.net/manual/function.mb-internal-encoding.php
159     */
160    public static function setDefaultEncoding(Encoding $encoding): void
161    {
162        $result = mb_internal_encoding($encoding->name);
163        if (!$result) {
164            throw new ArgumentException('$encoding');
165        }
166
167        self::$defaultEncoding = $encoding;
168    }
169
170    /**
171     * ASCIIエンコーディング。
172     *
173     * @return Encoding
174     */
175    public static function getAscii(): Encoding
176    {
177        return new Encoding(self::ENCODE_ASCII);
178    }
179
180    /**
181     * UTF8エンコーディング。
182     *
183     * @return Encoding
184     */
185    public static function getUtf8(): Encoding
186    {
187        return new Encoding(self::ENCODE_UTF8);
188    }
189
190    /**
191     * UTF16エンコーディング。
192     *
193     * @return Encoding
194     */
195    public static function getUtf16(): Encoding
196    {
197        return new Encoding(self::ENCODE_UTF16_DEFAULT);
198    }
199
200    /**
201     * UTF32エンコーディング。
202     *
203     * @return Encoding
204     */
205    public static function getUtf32(): Encoding
206    {
207        return new Encoding(self::ENCODE_UTF32_DEFAULT);
208    }
209
210    /**
211     * SJISエンコーディング。
212     *
213     * @return Encoding
214     */
215    public static function getShiftJis(): Encoding
216    {
217        return new Encoding(self::ENCODE_SJIS_DEFAULT);
218    }
219
220    /**
221     * 文字列(デフォルトエンコーディング)を現在のエンコーディングへ変換。
222     *
223     * @param string $input
224     * @return Binary
225     * @throws EncodingException
226     * @see https://www.php.net/manual/function.mb-convert-encoding.php
227     */
228    public function getBinary(string $input): Binary
229    {
230        $default = self::getDefaultEncoding();
231        if ($default->name === $this->name) {
232            return new Binary($input);
233        }
234
235        try {
236            $output = mb_convert_encoding($input, $this->name, $default->name);
237            if ($output === false) { //@phpstan-ignore-line [PHP_VERSION]
238                throw new EncodingException();
239            }
240            return new Binary($output);
241        } catch (ValueError $err) {
242            Throws::reThrow(EncodingException::class, $err);
243        }
244    }
245
246    /**
247     * 現在のエンコーディングデータを文字列(デフォルトエンコーディング)へ変換。
248     *
249     * @param Binary $input
250     * @return string
251     * @throws EncodingException
252     * @see https://www.php.net/manual/function.mb-convert-encoding.php
253     */
254    public function toString(Binary $input): string
255    {
256        $default = self::getDefaultEncoding();
257        if ($default->name === $this->name) {
258            return $input->raw;
259        }
260
261        try {
262            $output = mb_convert_encoding($input->raw, $default->name, $this->name);
263            if ($output === false) { //@phpstan-ignore-line
264                throw new EncodingException();
265            }
266            return $output;
267        } catch (ValueError $err) {
268            Throws::reThrow(EncodingException::class, $err);
269        }
270    }
271
272    /**
273     * 現在のエンコーディング・タイプのエイリアスを取得。
274     *
275     * @return string[]
276     * @see https://www.php.net/manual/function.mb-encoding-aliases.php
277     */
278    public static function getAliasNames(string $encoding): array
279    {
280        $names = Throws::wrap(ValueError::class, EncodingException::class, fn () => mb_encoding_aliases($encoding));
281        if ($names === false) { //@phpstan-ignore-line [PHP_VERSION]
282            throw new EncodingException($encoding);
283        }
284
285        return $names;
286    }
287
288    /**
289     * 現在のエンコーディングで有効か。
290     *
291     * @param Binary $input
292     * @return bool 有効か。
293     */
294    public function isValid(Binary $input): bool
295    {
296        return mb_check_encoding($input->raw, $this->name);
297    }
298
299    /**
300     * 現在のエンコーディングからBOMを取得する。
301     *
302     * @return Binary エンコーディング対するBOM。対応しない場合空のバイナリ。
303     */
304    public function getByteOrderMark(): Binary
305    {
306        //wikipedia の bom 見たけどわからん。
307        //TODO: 定義しているエンコーディングともあわんし、どうしたもんか
308        $bomMap = [
309            'UTF-8' => "\xEF\xBB\xBF",
310            'UTF-16BE' => "\xFE\xFF",
311            'UTF-16LE' => "\xFF\xFE",
312            'UTF-32BE' => "\x00\x00\xFE\xFF",
313            'UTF-32LE' => "\xFF\xFE\x00\x00",
314        ];
315
316        if (isset($bomMap[$this->name])) {
317            return new Binary($bomMap[$this->name]);
318        }
319
320        return new Binary('');
321    }
322
323    #endregion
324}