Passed
Push — develop ( a3a182...5b7e65 )
by nguereza
02:13
created

Str::toAttribute()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 3
nop 1
dl 0
loc 26
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Stdlib
5
 *
6
 * Platine Stdlib is a the collection of frequently used php features
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine Stdlib
11
 *
12
 * Permission is hereby granted, free of charge, to any person obtaining a copy
13
 * of this software and associated documentation files (the "Software"), to deal
14
 * in the Software without restriction, including without limitation the rights
15
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
 * copies of the Software, and to permit persons to whom the Software is
17
 * furnished to do so, subject to the following conditions:
18
 *
19
 * The above copyright notice and this permission notice shall be included in all
20
 * copies or substantial portions of the Software.
21
 *
22
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
 * SOFTWARE.
29
 */
30
31
/**
32
 *  @file Str.php
33
 *
34
 *  The String helper class
35
 *
36
 *  @package    Platine\Stdlib\Helper
37
 *  @author Platine Developers Team
38
 *  @copyright  Copyright (c) 2020
39
 *  @license    http://opensource.org/licenses/MIT  MIT License
40
 *  @link   http://www.iacademy.cf
41
 *  @version 1.0.0
42
 *  @filesource
43
 */
44
45
declare(strict_types=1);
46
47
namespace Platine\Stdlib\Helper;
48
49
use DateTime;
50
use DateTimeInterface;
51
use JsonSerializable;
52
use Throwable;
53
use Traversable;
54
55
/**
56
 * Class Str
57
 * @package Platine\Stdlib\Helper
58
 */
59
class Str
60
{
61
    /**
62
     * The cache of snake-cased words.
63
     *
64
     * @var array<string, string>
65
     */
66
    protected static array $snakeCache = [];
67
68
    /**
69
     * The cache of camel-cased words.
70
     *
71
     * @var array<string, string>
72
     */
73
    protected static array $camelCache = [];
74
75
    /**
76
     * The cache of studly-cased words.
77
     *
78
     * @var array<string, string>
79
     */
80
    protected static array $studlyCache = [];
81
82
    /**
83
     * Convert an UTF-8 value to ASCII.
84
     * @param string $value
85
     * @return string
86
     */
87
    public static function toAscii(string $value): string
88
    {
89
        foreach (self::getChars() as $key => $val) {
90
            $value = str_replace($val, $key, $value);
91
        }
92
93
        return (string)preg_replace('/[^\x20-\x7E]/u', '', $value);
94
    }
95
96
    /**
97
     * Convert to camel case
98
     * @param string $value
99
     * @param bool $lcfirst
100
     * @return string
101
     */
102
    public static function camel(string $value, bool $lcfirst = true): string
103
    {
104
        if (isset(self::$camelCache[$value])) {
105
            return self::$camelCache[$value];
106
        }
107
108
        $studly = static::studly($value);
109
        return self::$camelCache[$value] = ($lcfirst ? lcfirst($studly) : $studly);
110
    }
111
112
    /**
113
     * Convert an string to array
114
     * @param string $value
115
     * @param string $delimiter
116
     * @param int $limit
117
     * @return array<string>
118
     */
119
    public static function toArray(string $value, string $delimiter = ', ', int $limit = 0): array
120
    {
121
        $string = trim($value, $delimiter . ' ');
122
        if ($string === '') {
123
            return [];
124
        }
125
126
        $values = [];
127
        /** @var array<string> $rawList */
128
        $rawList = $limit < 1
129
                ? (array) explode($delimiter, $string)
130
                : (array) explode($delimiter, $string, $limit);
131
132
        foreach ($rawList as $val) {
133
            $val = trim($val);
134
            if ($val !== '') {
135
                $values[] = $val;
136
            }
137
        }
138
139
        return $values;
140
    }
141
142
    /**
143
     * Determine if a given string contains a given sub string.
144
     * @param string $value
145
     * @param string|array<mixed> $needles
146
     * @return bool
147
     */
148
    public static function contains(string $value, $needles): bool
149
    {
150
        if (!is_array($needles)) {
151
            $needles = [$needles];
152
        }
153
154
        foreach ($needles as $needle) {
155
            if ($needle !== '' && strpos($needle, $value) !== false) {
156
                return true;
157
            }
158
        }
159
160
        return false;
161
    }
162
163
    /**
164
     * Determine if a given string ends with a given sub string.
165
     * @param string $value
166
     * @param string|array<mixed> $needles
167
     * @return bool
168
     */
169
    public static function endsWith(string $value, $needles): bool
170
    {
171
        if (!is_array($needles)) {
172
            $needles = [$needles];
173
        }
174
175
        foreach ($needles as $needle) {
176
            if ($value === (string) substr($needle, -strlen($value))) {
177
                return true;
178
            }
179
        }
180
181
        return false;
182
    }
183
184
    /**
185
     * Determine if a given string starts with a given sub string.
186
     * @param string $value
187
     * @param string|array<mixed> $needles
188
     * @return bool
189
     */
190
    public static function startsWith(string $value, $needles): bool
191
    {
192
        if (!is_array($needles)) {
193
            $needles = [$needles];
194
        }
195
196
        foreach ($needles as $needle) {
197
            if ($needle !== '' && strpos($needle, $value) === 0) {
198
                return true;
199
            }
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * Return the first line of multi line string
207
     * @param string $value
208
     * @return string
209
     */
210
    public static function firstLine(string $value): string
211
    {
212
        $str = trim($value);
213
214
        if ($str === '') {
215
            return '';
216
        }
217
218
        if (strpos($str, "\n") > 0) {
219
            $parts = explode("\n", $str);
220
221
            return $parts[0] ?? '';
222
        }
223
224
        return $str;
225
    }
226
227
    /**
228
     * Cap a string with a single instance of a given value.
229
     * @param string $value
230
     * @param string $cap
231
     * @return string
232
     */
233
    public static function finish(string $value, string $cap): string
234
    {
235
        $quoted = preg_quote($cap, '/');
236
237
        return (string) preg_replace('/(?:' . $quoted . ')+$/', '', $value)
238
                . $cap;
239
    }
240
241
    /**
242
     * Determine if a given string matches a given pattern.
243
     * @param string $pattern
244
     * @param string $value
245
     * @return bool
246
     */
247
    public static function is(string $pattern, string $value): bool
248
    {
249
        if ($pattern === $value) {
250
            return true;
251
        }
252
253
        $quoted = preg_quote($pattern, '#');
254
255
        // Asterisks are translated into zero-or-more regular expression wildcards
256
        // to make it convenient to check if the strings starts with the given
257
        // pattern such as "library/*", making any string check convenient.
258
        $cleanQuoted = str_replace('\*', '.*', $quoted);
259
260
        return (bool)preg_match('#^' . $cleanQuoted . '\z#', $value);
261
    }
262
263
    /**
264
     * Return the length of the given string
265
     * @param string|int $value
266
     * @param string $encode
267
     * @return int
268
     */
269
    public static function length($value, string $encode = 'UTF-8'): int
270
    {
271
        if (!is_string($value)) {
272
            $value = (string) $value;
273
        }
274
275
        $length = mb_strlen($value, $encode);
276
277
        return $length !== false ? $length : -1;
278
    }
279
280
281
    /**
282
     * Add padding to string
283
     * @param string|int $value
284
     * @param int $length
285
     * @param string $padStr
286
     * @param int $type
287
     * @return string
288
     */
289
    public static function pad(
290
        $value,
291
        int $length,
292
        string $padStr = ' ',
293
        int $type = STR_PAD_BOTH
294
    ): string {
295
        if (!is_string($value)) {
296
            $value = (string) $value;
297
        }
298
299
        return $length > 0
300
                ? str_pad($value, $length, $padStr, $type)
301
                : $value;
302
    }
303
304
    /**
305
     * Add padding to string to left
306
     * @param string|int $value
307
     * @param int $length
308
     * @param string $padStr
309
     * @return string
310
     */
311
    public static function padLeft(
312
        $value,
313
        int $length,
314
        string $padStr = ' '
315
    ): string {
316
        return self::pad($value, $length, $padStr, STR_PAD_LEFT);
317
    }
318
319
    /**
320
     * Add padding to string to right
321
     * @param string|int $value
322
     * @param int $length
323
     * @param string $padStr
324
     * @return string
325
     */
326
    public static function padRight(
327
        $value,
328
        int $length,
329
        string $padStr = ' '
330
    ): string {
331
        return self::pad($value, $length, $padStr, STR_PAD_RIGHT);
332
    }
333
334
    /**
335
     * Repeat the given string $length times
336
     * @param string|int $value
337
     * @param int $length
338
     * @return string
339
     */
340
    public static function repeat($value, int $length = 1): string
341
    {
342
        if (!is_string($value)) {
343
            $value = (string) $value;
344
        }
345
346
        return str_repeat($value, $length);
347
    }
348
349
    /**
350
     * Limit the length of given string
351
     * @param string $value
352
     * @param int $length
353
     * @param string $end
354
     * @return string
355
     */
356
    public static function limit(string $value, int $length = 100, string $end = '...'): string
357
    {
358
        if (mb_strwidth($value, 'UTF-8') <= $length) {
359
            return $value;
360
        }
361
362
        return rtrim(mb_strimwidth($value, 0, $length, '', 'UTF-8')) . $end;
363
    }
364
365
    /**
366
     * Limit the number of words in a string.
367
     * @param string $value
368
     * @param int $length
369
     * @param string $end
370
     * @return string
371
     */
372
    public static function words(string $value, int $length = 100, string $end = '...'): string
373
    {
374
        $matches = [];
375
        preg_match('/^\s*+(?:\S++\s*+){1,' . $length . '}/u', $value, $matches);
376
377
        if (!isset($matches[0]) || strlen($value) === strlen($matches[0])) {
378
            return $value;
379
        }
380
381
        return rtrim($matches[0]) . $end;
382
    }
383
384
    /**
385
     * Replace the first match of the given string
386
     * @param string $search
387
     * @param string $replace
388
     * @param string $value
389
     * @return string
390
     */
391
    public static function replaceFirst(string $search, string $replace, string $value): string
392
    {
393
        $pos = strpos($value, $search);
394
        if ($pos !== false) {
395
            return substr_replace($value, $replace, $pos, strlen($search));
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr_replace($v... $pos, strlen($search)) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
396
        }
397
398
        return $value;
399
    }
400
401
    /**
402
     * Replace the last match of the given string
403
     * @param string $search
404
     * @param string $replace
405
     * @param string $value
406
     * @return string
407
     */
408
    public static function replaceLast(string $search, string $replace, string $value): string
409
    {
410
        $pos = strrpos($value, $search);
411
412
        if ($pos !== false) {
413
            return substr_replace($value, $replace, $pos, strlen($search));
0 ignored issues
show
Bug Best Practice introduced by
The expression return substr_replace($v... $pos, strlen($search)) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
414
        }
415
416
        return $value;
417
    }
418
419
    /**
420
     * Put the string to title format
421
     * @param string $value
422
     * @return string
423
     */
424
    public static function title(string $value): string
425
    {
426
        return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
427
    }
428
429
    /**
430
     * Generate a friendly "slug" from a given string.
431
     * @param string $value
432
     * @param string $separator
433
     * @return string
434
     */
435
    public static function slug(string $value, string $separator = '-'): string
436
    {
437
        $title = self::toAscii($value);
438
439
        // Convert all dashes/underscores into separator
440
        $flip = $separator === '-' ? '_' : '-';
441
442
        $utf8 = (string) preg_replace('![' . preg_quote($flip) . ']+!u', $separator, $title);
443
444
        // Remove all characters that are not the separator, letters, numbers,
445
        // or whitespace.
446
        $alphaNum = (string) preg_replace(
447
            '![^' . preg_quote($separator) . '\pL\pN\s]+!u',
448
            '',
449
            mb_strtolower($utf8)
450
        );
451
452
        // Replace all separator characters and whitespace by a single separator
453
        $removeWhitespace = (string) preg_replace(
454
            '![' . preg_quote($separator) . '\s]+!u',
455
            $separator,
456
            $alphaNum
457
        );
458
459
        return trim($removeWhitespace, $separator);
460
    }
461
462
    /**
463
     * Convert a string to snake case.
464
     * @param string $value
465
     * @param string $separator
466
     * @return string
467
     */
468
    public static function snake(string $value, string $separator = '_'): string
469
    {
470
        $key = $value . $separator;
471
        if (isset(self::$snakeCache[$key])) {
472
            return self::$snakeCache[$key];
473
        }
474
475
        if (!ctype_lower($value)) {
476
            $replace = (string) preg_replace('/\s+/', '', $value);
477
478
            $value = strtolower((string) preg_replace(
479
                '/(.)(?=[A-Z])/',
480
                '$1' . $separator,
481
                $replace
482
            ));
483
        }
484
485
        return self::$snakeCache[$key] = $value;
486
    }
487
488
    /**
489
     * Convert a value to studly caps case.
490
     * @param string $value
491
     * @return string
492
     */
493
    public static function studly(string $value): string
494
    {
495
        $key = $value;
496
        if (isset(self::$studlyCache[$key])) {
497
            return self::$studlyCache[$key];
498
        }
499
500
        $val = ucwords(str_replace(['-', '_'], ' ', $value));
501
502
        return self::$studlyCache[$key] = str_replace(' ', '', $val);
503
    }
504
505
    /**
506
     * Returns the portion of string specified by the start and
507
     * length parameters.
508
     *
509
     * @param string $value
510
     * @param int $start
511
     * @param int|null $length
512
     * @return string
513
     */
514
    public static function substr(string $value, int $start = 0, ?int $length = null): string
515
    {
516
        return mb_substr($value, $start, $length, 'UTF-8');
517
    }
518
519
    /**
520
     * Make a string's first character to upper case.
521
     * @param string $value
522
     * @return string
523
     */
524
    public static function ucfirst(string $value): string
525
    {
526
        return static::upper(
527
            static::substr($value, 0, 1)
528
        ) . static::substr($value, 1);
529
    }
530
531
    /**
532
     * Split the string by length part
533
     * @param string $value
534
     * @param int $length
535
     * @return array<int, string>
536
     */
537
    public static function split(string $value, int $length = 1): array
538
    {
539
        if ($length < 1) {
540
            return [];
541
        }
542
543
        if (self::isAscii($value)) {
544
            $res = str_split($value, $length);
545
            if ($res === false) {
546
                return [];
547
            }
548
549
            return $res;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $res could return the type true which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
550
        }
551
552
        if (mb_strlen($value) <= $length) {
553
            return [$value];
554
        }
555
        $matches = [];
556
        preg_match_all(
557
            '/.{' . $length . '}|[^\x00]{1,' . $length . '}$/us',
558
            $value,
559
            $matches
560
        );
561
562
        return $matches[0];
563
    }
564
565
    /**
566
     * Check whether the given string contains only ASCII chars
567
     * @param string $value
568
     * @return bool
569
     */
570
    public static function isAscii(string $value): bool
571
    {
572
        return (bool)!preg_match('/[^\x00-\x7F]/S', $value);
573
    }
574
575
    /**
576
     * Put string to lower case
577
     * @param string $value
578
     * @return string
579
     */
580
    public static function lower(string $value): string
581
    {
582
        return mb_strtolower($value, 'UTF-8');
583
    }
584
585
    /**
586
     * Put string to upper case
587
     * @param string $value
588
     * @return string
589
     */
590
    public static function upper(string $value): string
591
    {
592
        return mb_strtoupper($value, 'UTF-8');
593
    }
594
595
    /**
596
     * Return the unique ID
597
     * @param int $length
598
     *
599
     * @return string
600
     */
601
    public static function uniqId(int $length = 13): string
602
    {
603
        $bytes = random_bytes((int) ceil($length / 2));
604
605
        return (string)substr(bin2hex($bytes), 0, $length);
606
    }
607
608
    /**
609
     * Put array to HTML attributes
610
     * @param array<string, mixed> $attributes
611
     * @return string
612
     */
613
    public static function toAttribute(array $attributes): string
614
    {
615
        if (empty($attributes)) {
616
            return '';
617
        }
618
619
        // handle boolean, array, & html special chars
620
        array_walk($attributes, function (&$value, $key) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

620
        array_walk($attributes, function (&$value, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
621
            $value = is_bool($value) ? $value ? 'true' : 'false' : $value;
622
            $value = is_array($value) ? implode(' ', $value) : $value;
623
            $value = trim($value);
624
            $value = htmlspecialchars($value);
625
        });
626
627
        // remove empty elements
628
        $emptyAttributes = array_filter($attributes, function ($value) {
629
            return strlen($value) > 0;
630
        });
631
632
        if (empty($emptyAttributes)) {
633
            return '';
634
        }
635
636
        $compiled = implode('="%s" ', array_keys($emptyAttributes)) . '="%s"';
637
638
        return vsprintf($compiled, array_values($emptyAttributes));
639
    }
640
641
    /**
642
     * Generate random string value
643
     * @param int $length
644
     * @return string
645
     */
646
    public static function random(int $length = 16): string
647
    {
648
        $string = '';
649
        while (($len = strlen($string)) < $length) {
650
            $size = $length - $len;
651
            $bytes = random_bytes($size);
652
653
            $string .= substr(
654
                str_replace(['/', '+', '='], '', base64_encode($bytes)),
655
                0,
656
                $size
657
            );
658
        }
659
660
        return $string;
661
    }
662
663
    /**
664
     * Generates a random string of a given type and length. Possible
665
     * values for the first argument ($type) are:
666
     *  - alnum    - alpha-numeric characters (including capitals)
667
     *  - alpha    - alphabetical characters (including capitals)
668
     *  - hexdec   - hexadecimal characters, 0-9 plus a-f
669
     *  - numeric  - digit characters, 0-9
670
     *  - nozero   - digit characters, 1-9
671
     *  - distinct - clearly distinct alpha-numeric characters.
672
     * @param string $type
673
     * @param int $length
674
     * @return string
675
     */
676
    public static function randomString(string $type = 'alnum', int $length = 8): string
677
    {
678
        $utf8 = false;
679
680
        switch ($type) {
681
            case 'alnum':
682
                $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
683
                break;
684
            case 'alpha':
685
                $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
686
                break;
687
            case 'lowalnum':
688
                $pool = '0123456789abcdefghijklmnopqrstuvwxyz';
689
                break;
690
            case 'hexdec':
691
                $pool = '0123456789abcdef';
692
                break;
693
            case 'numeric':
694
                $pool = '0123456789';
695
                break;
696
            case 'nozero':
697
                $pool = '123456789';
698
                break;
699
            case 'distinct':
700
                $pool = '2345679ACDEFHJKLMNPRSTUVWXYZ';
701
                break;
702
            default:
703
                $pool = (string)$type;
704
                $utf8 = !self::isAscii($pool);
705
                break;
706
        }
707
708
        // Split the pool into an array of characters
709
        $pool = $utf8 ? self::split($pool, 1) : str_split($pool, 1);
710
        // Largest pool key
711
        $max = count($pool) - 1;
0 ignored issues
show
Bug introduced by
It seems like $pool can also be of type boolean; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

711
        $max = count(/** @scrutinizer ignore-type */ $pool) - 1;
Loading history...
712
713
        $str = '';
714
        for ($i = 0; $i < $length; $i++) {
715
            // Select a random character from the pool and add it to the string
716
            $str .= $pool[random_int(0, $max)];
717
        }
718
719
        // Make sure alnum strings contain at least one letter and one digit
720
        if ($type === 'alnum' && $length > 1) {
721
            if (ctype_alpha($str)) {
722
                // Add a random digit
723
                $str[random_int(0, $length - 1)] = chr(random_int(48, 57));
724
            } elseif (ctype_digit($str)) {
725
                // Add a random letter
726
                $str[random_int(0, $length - 1)] = chr(random_int(65, 90));
727
            }
728
        }
729
730
        return $str;
731
    }
732
733
    /**
734
     * Create a simple random token-string
735
     * @param int $length
736
     * @param string $salt
737
     * @return string
738
     */
739
    public static function randomToken(int $length = 24, string $salt = ''): string
740
    {
741
        $string = '';
742
        $chars  = '0456789abc1def2ghi3jkl';
743
        $maxVal = strlen($chars) - 1;
744
745
        for ($i = 0; $i < $length; ++$i) {
746
            $string .= $chars[random_int(0, $maxVal)];
747
        }
748
749
        return md5($string . $salt);
750
    }
751
752
    /**
753
     * Convert the given value to string representation
754
     * @param mixed $value
755
     * @return string
756
     */
757
    public static function stringify($value): string
758
    {
759
        if ($value === null) {
760
            return 'null';
761
        }
762
763
        if (is_bool($value)) {
764
            return $value ? 'true' : 'false';
765
        }
766
767
        if (is_string($value)) {
768
            return $value;
769
        }
770
771
        if (is_scalar($value)) {
772
            return (string) $value;
773
        }
774
775
        if (is_array($value)) {
776
            return self::stringifyArray($value);
777
        }
778
779
        if (is_object($value)) {
780
            return self::stringifyObject($value);
781
        }
782
783
        if (is_resource($value)) {
784
            return sprintf('resource<%s>', get_resource_type($value));
785
        }
786
787
        return gettype($value);
788
    }
789
790
    /**
791
     * Convert the given array to string representation
792
     * @param array<mixed> $value
793
     * @return string
794
     */
795
    public static function stringifyArray(array $value): string
796
    {
797
        if (empty($value)) {
798
            return '[]';
799
        }
800
801
        $keys = array_keys($value);
802
        $values = array_values($value);
803
        [$firstKey] = $keys;
804
        $ignoreKeys = $firstKey === 0;
805
806
        return sprintf('[%s]', implode(', ', array_map(
807
            function ($key, $value) use ($ignoreKeys) {
808
                return $ignoreKeys
809
                        ? self::stringify($value)
810
                        : sprintf(
811
                            '%s => %s',
812
                            self::stringify($key),
813
                            self::stringify($value)
814
                        );
815
            },
816
            $keys,
817
            $values
818
        )));
819
    }
820
821
    /**
822
     * Convert the given object to string representation
823
     * @param object $value
824
     * @return string
825
     */
826
    public static function stringifyObject(object $value): string
827
    {
828
        $valueClass = get_class($value);
829
830
        if ($value instanceof Throwable) {
831
            return sprintf(
832
                '%s { "%s", %s, %s #%s }',
833
                $valueClass,
834
                $value->getMessage(),
835
                $value->getCode(),
836
                $value->getFile(),
837
                $value->getLine()
838
            );
839
        }
840
841
        if (method_exists($value, '__toString')) {
842
            return sprintf('%s { %s }', $valueClass, $value->__toString());
843
        }
844
845
        if (method_exists($value, 'toString')) {
846
            return sprintf('%s { %s }', $valueClass, $value->toString());
847
        }
848
849
        if ($value instanceof Traversable) {
850
            return sprintf(
851
                '%s %s',
852
                $valueClass,
853
                self::stringifyArray(iterator_to_array($value))
854
            );
855
        }
856
857
        if ($value instanceof DateTimeInterface) {
858
            return sprintf(
859
                '%s { %s }',
860
                $valueClass,
861
                $value->format(DateTime::ATOM)
862
            );
863
        }
864
865
        if ($value instanceof JsonSerializable) {
866
            return sprintf(
867
                '%s {%s}',
868
                $valueClass,
869
                trim((string) json_encode($value->jsonSerialize()), '{}')
870
            );
871
        }
872
873
        return $valueClass;
874
    }
875
876
    /**
877
     * Return the user ip address
878
     * @return string
879
     */
880
    public static function ip(): string
881
    {
882
        $ip = '127.0.0.1';
883
884
        $ipServerVars = [
885
            'REMOTE_ADDR',
886
            'HTTP_CLIENT_IP',
887
            'HTTP_X_FORWARDED_FOR',
888
            'HTTP_X_FORWARDED',
889
            'HTTP_FORWARDED_FOR',
890
            'HTTP_FORWARDED'
891
        ];
892
893
        foreach ($ipServerVars as $var) {
894
            //https://bugs.php.net/bug.php?id=49184 can
895
            // not use filter_input(INPUT_SERVER, $var);
896
897
            if (isset($_SERVER[$var])) {
898
                $ip = htmlspecialchars(
899
                    strip_tags((string) $_SERVER[$var]),
900
                    ENT_COMPAT,
901
                    'UTF-8'
902
                );
903
                break;
904
            }
905
        }
906
907
        // Strip any secondary IP etc from the IP address
908
        if (strpos($ip, ',') > 0) {
909
            $ip = substr($ip, 0, strpos($ip, ','));
910
        }
911
912
        return $ip;
913
    }
914
915
    /**
916
     * Return the ASCII replacement
917
     * @return array<string, array<string>>
918
     */
919
    private static function getChars(): array
920
    {
921
        return [
922
            '0'    => ['°', '₀'],
923
            '1'    => ['¹', '₁'],
924
            '2'    => ['²', '₂'],
925
            '3'    => ['³', '₃'],
926
            '4'    => ['⁴', '₄'],
927
            '5'    => ['⁵', '₅'],
928
            '6'    => ['⁶', '₆'],
929
            '7'    => ['⁷', '₇'],
930
            '8'    => ['⁸', '₈'],
931
            '9'    => ['⁹', '₉'],
932
            'a'    => [
933
                'à',
934
                'á',
935
                'ả',
936
                'ã',
937
                'ạ',
938
                'ă',
939
                'ắ',
940
                'ằ',
941
                'ẳ',
942
                'ẵ',
943
                'ặ',
944
                'â',
945
                'ấ',
946
                'ầ',
947
                'ẩ',
948
                'ẫ',
949
                'ậ',
950
                'ā',
951
                'ą',
952
                'å',
953
                'α',
954
                'ά',
955
                'ἀ',
956
                'ἁ',
957
                'ἂ',
958
                'ἃ',
959
                'ἄ',
960
                'ἅ',
961
                'ἆ',
962
                'ἇ',
963
                'ᾀ',
964
                'ᾁ',
965
                'ᾂ',
966
                'ᾃ',
967
                'ᾄ',
968
                'ᾅ',
969
                'ᾆ',
970
                'ᾇ',
971
                'ὰ',
972
                'ά',
973
                'ᾰ',
974
                'ᾱ',
975
                'ᾲ',
976
                'ᾳ',
977
                'ᾴ',
978
                'ᾶ',
979
                'ᾷ',
980
                'а',
981
                'أ',
982
                'အ',
983
                'ာ',
984
                'ါ',
985
                'ǻ',
986
                'ǎ',
987
                'ª',
988
                'ა',
989
                'अ'
990
            ],
991
            'b'    => ['б', 'β', 'Ъ', 'Ь', 'ب', 'ဗ', 'ბ'],
992
            'c'    => ['ç', 'ć', 'č', 'ĉ', 'ċ'],
993
            'd'    => ['ď', 'ð', 'đ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ᵭ', 'ᶁ', 'ᶑ', 'д', 'δ', 'د', 'ض', 'ဍ', 'ဒ', 'დ'],
994
            'e'    => [
995
                'é',
996
                'è',
997
                'ẻ',
998
                'ẽ',
999
                'ẹ',
1000
                'ê',
1001
                'ế',
1002
                'ề',
1003
                'ể',
1004
                'ễ',
1005
                'ệ',
1006
                'ë',
1007
                'ē',
1008
                'ę',
1009
                'ě',
1010
                'ĕ',
1011
                'ė',
1012
                'ε',
1013
                'έ',
1014
                'ἐ',
1015
                'ἑ',
1016
                'ἒ',
1017
                'ἓ',
1018
                'ἔ',
1019
                'ἕ',
1020
                'ὲ',
1021
                'έ',
1022
                'е',
1023
                'ё',
1024
                'э',
1025
                'є',
1026
                'ə',
1027
                'ဧ',
1028
                'ေ',
1029
                'ဲ',
1030
                'ე',
1031
                'ए'
1032
            ],
1033
            'f'    => ['ф', 'φ', 'ف', 'ƒ', 'ფ'],
1034
            'g'    => ['ĝ', 'ğ', 'ġ', 'ģ', 'г', 'ґ', 'γ', 'ج', 'ဂ', 'გ'],
1035
            'h'    => ['ĥ', 'ħ', 'η', 'ή', 'ح', 'ه', 'ဟ', 'ှ', 'ჰ'],
1036
            'i'    => [
1037
                'í',
1038
                'ì',
1039
                'ỉ',
1040
                'ĩ',
1041
                'ị',
1042
                'î',
1043
                'ï',
1044
                'ī',
1045
                'ĭ',
1046
                'į',
1047
                'ı',
1048
                'ι',
1049
                'ί',
1050
                'ϊ',
1051
                'ΐ',
1052
                'ἰ',
1053
                'ἱ',
1054
                'ἲ',
1055
                'ἳ',
1056
                'ἴ',
1057
                'ἵ',
1058
                'ἶ',
1059
                'ἷ',
1060
                'ὶ',
1061
                'ί',
1062
                'ῐ',
1063
                'ῑ',
1064
                'ῒ',
1065
                'ΐ',
1066
                'ῖ',
1067
                'ῗ',
1068
                'і',
1069
                'ї',
1070
                'и',
1071
                'ဣ',
1072
                'ိ',
1073
                'ီ',
1074
                'ည်',
1075
                'ǐ',
1076
                'ი',
1077
                'इ'
1078
            ],
1079
            'j'    => ['ĵ', 'ј', 'Ј', 'ჯ'],
1080
            'k'    => ['ķ', 'ĸ', 'к', 'κ', 'Ķ', 'ق', 'ك', 'က', 'კ', 'ქ'],
1081
            'l'    => ['ł', 'ľ', 'ĺ', 'ļ', 'ŀ', 'л', 'λ', 'ل', 'လ', 'ლ'],
1082
            'm'    => ['м', 'μ', 'م', 'မ', 'მ'],
1083
            'n'    => ['ñ', 'ń', 'ň', 'ņ', 'ʼn', 'ŋ', 'ν', 'н', 'ن', 'န', 'ნ'],
1084
            'o'    => [
1085
                'ó',
1086
                'ò',
1087
                'ỏ',
1088
                'õ',
1089
                'ọ',
1090
                'ô',
1091
                'ố',
1092
                'ồ',
1093
                'ổ',
1094
                'ỗ',
1095
                'ộ',
1096
                'ơ',
1097
                'ớ',
1098
                'ờ',
1099
                'ở',
1100
                'ỡ',
1101
                'ợ',
1102
                'ø',
1103
                'ō',
1104
                'ő',
1105
                'ŏ',
1106
                'ο',
1107
                'ὀ',
1108
                'ὁ',
1109
                'ὂ',
1110
                'ὃ',
1111
                'ὄ',
1112
                'ὅ',
1113
                'ὸ',
1114
                'ό',
1115
                'о',
1116
                'و',
1117
                'θ',
1118
                'ို',
1119
                'ǒ',
1120
                'ǿ',
1121
                'º',
1122
                'ო',
1123
                'ओ'
1124
            ],
1125
            'p'    => ['п', 'π', 'ပ', 'პ'],
1126
            'q'    => ['ყ'],
1127
            'r'    => ['ŕ', 'ř', 'ŗ', 'р', 'ρ', 'ر', 'რ'],
1128
            's'    => ['ś', 'š', 'ş', 'с', 'σ', 'ș', 'ς', 'س', 'ص', 'စ', 'ſ', 'ს'],
1129
            't'    => ['ť', 'ţ', 'т', 'τ', 'ț', 'ت', 'ط', 'ဋ', 'တ', 'ŧ', 'თ', 'ტ'],
1130
            'u'    => [
1131
                'ú',
1132
                'ù',
1133
                'ủ',
1134
                'ũ',
1135
                'ụ',
1136
                'ư',
1137
                'ứ',
1138
                'ừ',
1139
                'ử',
1140
                'ữ',
1141
                'ự',
1142
                'û',
1143
                'ū',
1144
                'ů',
1145
                'ű',
1146
                'ŭ',
1147
                'ų',
1148
                'µ',
1149
                'у',
1150
                'ဉ',
1151
                'ု',
1152
                'ူ',
1153
                'ǔ',
1154
                'ǖ',
1155
                'ǘ',
1156
                'ǚ',
1157
                'ǜ',
1158
                'უ',
1159
                'उ'
1160
            ],
1161
            'v'    => ['в', 'ვ', 'ϐ'],
1162
            'w'    => ['ŵ', 'ω', 'ώ', 'ဝ', 'ွ'],
1163
            'x'    => ['χ', 'ξ'],
1164
            'y'    => ['ý', 'ỳ', 'ỷ', 'ỹ', 'ỵ', 'ÿ', 'ŷ', 'й', 'ы', 'υ', 'ϋ', 'ύ', 'ΰ', 'ي', 'ယ'],
1165
            'z'    => ['ź', 'ž', 'ż', 'з', 'ζ', 'ز', 'ဇ', 'ზ'],
1166
            'aa'   => ['ع', 'आ'],
1167
            'ae'   => ['ä', 'æ', 'ǽ'],
1168
            'ai'   => ['ऐ'],
1169
            'at'   => ['@'],
1170
            'ch'   => ['ч', 'ჩ', 'ჭ'],
1171
            'dj'   => ['ђ', 'đ'],
1172
            'dz'   => ['џ', 'ძ'],
1173
            'ei'   => ['ऍ'],
1174
            'gh'   => ['غ', 'ღ'],
1175
            'ii'   => ['ई'],
1176
            'ij'   => ['ij'],
1177
            'kh'   => ['х', 'خ', 'ხ'],
1178
            'lj'   => ['љ'],
1179
            'nj'   => ['њ'],
1180
            'oe'   => ['ö', 'œ'],
1181
            'oi'   => ['ऑ'],
1182
            'oii'  => ['ऒ'],
1183
            'ps'   => ['ψ'],
1184
            'sh'   => ['ш', 'შ'],
1185
            'shch' => ['щ'],
1186
            'ss'   => ['ß'],
1187
            'sx'   => ['ŝ'],
1188
            'th'   => ['þ', 'ϑ', 'ث', 'ذ', 'ظ'],
1189
            'ts'   => ['ц', 'ც', 'წ'],
1190
            'ue'   => ['ü'],
1191
            'uu'   => ['ऊ'],
1192
            'ya'   => ['я'],
1193
            'yu'   => ['ю'],
1194
            'zh'   => ['ж', 'ჟ'],
1195
            '(c)'  => ['©'],
1196
            'A'    => [
1197
                'Á',
1198
                'À',
1199
                'Ả',
1200
                'Ã',
1201
                'Ạ',
1202
                'Ă',
1203
                'Ắ',
1204
                'Ằ',
1205
                'Ẳ',
1206
                'Ẵ',
1207
                'Ặ',
1208
                'Â',
1209
                'Ấ',
1210
                'Ầ',
1211
                'Ẩ',
1212
                'Ẫ',
1213
                'Ậ',
1214
                'Å',
1215
                'Ā',
1216
                'Ą',
1217
                'Α',
1218
                'Ά',
1219
                'Ἀ',
1220
                'Ἁ',
1221
                'Ἂ',
1222
                'Ἃ',
1223
                'Ἄ',
1224
                'Ἅ',
1225
                'Ἆ',
1226
                'Ἇ',
1227
                'ᾈ',
1228
                'ᾉ',
1229
                'ᾊ',
1230
                'ᾋ',
1231
                'ᾌ',
1232
                'ᾍ',
1233
                'ᾎ',
1234
                'ᾏ',
1235
                'Ᾰ',
1236
                'Ᾱ',
1237
                'Ὰ',
1238
                'Ά',
1239
                'ᾼ',
1240
                'А',
1241
                'Ǻ',
1242
                'Ǎ'
1243
            ],
1244
            'B'    => ['Б', 'Β', 'ब'],
1245
            'C'    => ['Ç', 'Ć', 'Č', 'Ĉ', 'Ċ'],
1246
            'D'    => ['Ď', 'Ð', 'Đ', 'Ɖ', 'Ɗ', 'Ƌ', 'ᴅ', 'ᴆ', 'Д', 'Δ'],
1247
            'E'    => [
1248
                'É',
1249
                'È',
1250
                'Ẻ',
1251
                'Ẽ',
1252
                'Ẹ',
1253
                'Ê',
1254
                'Ế',
1255
                'Ề',
1256
                'Ể',
1257
                'Ễ',
1258
                'Ệ',
1259
                'Ë',
1260
                'Ē',
1261
                'Ę',
1262
                'Ě',
1263
                'Ĕ',
1264
                'Ė',
1265
                'Ε',
1266
                'Έ',
1267
                'Ἐ',
1268
                'Ἑ',
1269
                'Ἒ',
1270
                'Ἓ',
1271
                'Ἔ',
1272
                'Ἕ',
1273
                'Έ',
1274
                'Ὲ',
1275
                'Е',
1276
                'Ё',
1277
                'Э',
1278
                'Є',
1279
                'Ə'
1280
            ],
1281
            'F'    => ['Ф', 'Φ'],
1282
            'G'    => ['Ğ', 'Ġ', 'Ģ', 'Г', 'Ґ', 'Γ'],
1283
            'H'    => ['Η', 'Ή', 'Ħ'],
1284
            'I'    => [
1285
                'Í',
1286
                'Ì',
1287
                'Ỉ',
1288
                'Ĩ',
1289
                'Ị',
1290
                'Î',
1291
                'Ï',
1292
                'Ī',
1293
                'Ĭ',
1294
                'Į',
1295
                'İ',
1296
                'Ι',
1297
                'Ί',
1298
                'Ϊ',
1299
                'Ἰ',
1300
                'Ἱ',
1301
                'Ἳ',
1302
                'Ἴ',
1303
                'Ἵ',
1304
                'Ἶ',
1305
                'Ἷ',
1306
                'Ῐ',
1307
                'Ῑ',
1308
                'Ὶ',
1309
                'Ί',
1310
                'И',
1311
                'І',
1312
                'Ї',
1313
                'Ǐ',
1314
                'ϒ'
1315
            ],
1316
            'K'    => ['К', 'Κ'],
1317
            'L'    => ['Ĺ', 'Ł', 'Л', 'Λ', 'Ļ', 'Ľ', 'Ŀ', 'ल'],
1318
            'M'    => ['М', 'Μ'],
1319
            'N'    => ['Ń', 'Ñ', 'Ň', 'Ņ', 'Ŋ', 'Н', 'Ν'],
1320
            'O'    => [
1321
                'Ó',
1322
                'Ò',
1323
                'Ỏ',
1324
                'Õ',
1325
                'Ọ',
1326
                'Ô',
1327
                'Ố',
1328
                'Ồ',
1329
                'Ổ',
1330
                'Ỗ',
1331
                'Ộ',
1332
                'Ơ',
1333
                'Ớ',
1334
                'Ờ',
1335
                'Ở',
1336
                'Ỡ',
1337
                'Ợ',
1338
                'Ø',
1339
                'Ō',
1340
                'Ő',
1341
                'Ŏ',
1342
                'Ο',
1343
                'Ό',
1344
                'Ὀ',
1345
                'Ὁ',
1346
                'Ὂ',
1347
                'Ὃ',
1348
                'Ὄ',
1349
                'Ὅ',
1350
                'Ὸ',
1351
                'Ό',
1352
                'О',
1353
                'Θ',
1354
                'Ө',
1355
                'Ǒ',
1356
                'Ǿ'
1357
            ],
1358
            'P'    => ['П', 'Π'],
1359
            'R'    => ['Ř', 'Ŕ', 'Р', 'Ρ', 'Ŗ'],
1360
            'S'    => ['Ş', 'Ŝ', 'Ș', 'Š', 'Ś', 'С', 'Σ'],
1361
            'T'    => ['Ť', 'Ţ', 'Ŧ', 'Ț', 'Т', 'Τ'],
1362
            'U'    => [
1363
                'Ú',
1364
                'Ù',
1365
                'Ủ',
1366
                'Ũ',
1367
                'Ụ',
1368
                'Ư',
1369
                'Ứ',
1370
                'Ừ',
1371
                'Ử',
1372
                'Ữ',
1373
                'Ự',
1374
                'Û',
1375
                'Ū',
1376
                'Ů',
1377
                'Ű',
1378
                'Ŭ',
1379
                'Ų',
1380
                'У',
1381
                'Ǔ',
1382
                'Ǖ',
1383
                'Ǘ',
1384
                'Ǚ',
1385
                'Ǜ'
1386
            ],
1387
            'V'    => ['В'],
1388
            'W'    => ['Ω', 'Ώ', 'Ŵ'],
1389
            'X'    => ['Χ', 'Ξ'],
1390
            'Y'    => ['Ý', 'Ỳ', 'Ỷ', 'Ỹ', 'Ỵ', 'Ÿ', 'Ῠ', 'Ῡ', 'Ὺ', 'Ύ', 'Ы', 'Й', 'Υ', 'Ϋ', 'Ŷ'],
1391
            'Z'    => ['Ź', 'Ž', 'Ż', 'З', 'Ζ'],
1392
            'AE'   => ['Ä', 'Æ', 'Ǽ'],
1393
            'CH'   => ['Ч'],
1394
            'DJ'   => ['Ђ'],
1395
            'DZ'   => ['Џ'],
1396
            'GX'   => ['Ĝ'],
1397
            'HX'   => ['Ĥ'],
1398
            'IJ'   => ['IJ'],
1399
            'JX'   => ['Ĵ'],
1400
            'KH'   => ['Х'],
1401
            'LJ'   => ['Љ'],
1402
            'NJ'   => ['Њ'],
1403
            'OE'   => ['Ö', 'Œ'],
1404
            'PS'   => ['Ψ'],
1405
            'SH'   => ['Ш'],
1406
            'SHCH' => ['Щ'],
1407
            'SS'   => ['ẞ'],
1408
            'TH'   => ['Þ'],
1409
            'TS'   => ['Ц'],
1410
            'UE'   => ['Ü'],
1411
            'YA'   => ['Я'],
1412
            'YU'   => ['Ю'],
1413
            'ZH'   => ['Ж'],
1414
            ' '    => [
1415
                "\xC2\xA0",
1416
                "\xE2\x80\x80",
1417
                "\xE2\x80\x81",
1418
                "\xE2\x80\x82",
1419
                "\xE2\x80\x83",
1420
                "\xE2\x80\x84",
1421
                "\xE2\x80\x85",
1422
                "\xE2\x80\x86",
1423
                "\xE2\x80\x87",
1424
                "\xE2\x80\x88",
1425
                "\xE2\x80\x89",
1426
                "\xE2\x80\x8A",
1427
                "\xE2\x80\xAF",
1428
                "\xE2\x81\x9F",
1429
                "\xE3\x80\x80"
1430
            ],
1431
        ];
1432
    }
1433
}
1434