Str::studly()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 10
rs 10
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   https://www.platine-php.com
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 Stringable;
53
use Throwable;
54
use Traversable;
55
56
/**
57
 * @class Str
58
 * @package Platine\Stdlib\Helper
59
 */
60
class Str
61
{
62
    /**
63
     * The cache of snake-cased words.
64
     *
65
     * @var array<string, string>
66
     */
67
    protected static array $snakeCache = [];
68
69
    /**
70
     * The cache of camel-cased words.
71
     *
72
     * @var array<string, string>
73
     */
74
    protected static array $camelCache = [];
75
76
    /**
77
     * The cache of studly-cased words.
78
     *
79
     * @var array<string, string>
80
     */
81
    protected static array $studlyCache = [];
82
83
    /**
84
     * Convert an UTF-8 value to ASCII.
85
     * @param string $value
86
     * @return string
87
     */
88
    public static function toAscii(string $value): string
89
    {
90
        foreach (self::getChars() as $key => $val) {
91
            $value = str_replace($val, (string) $key, $value);
92
        }
93
94
        return (string)preg_replace('/[^\x20-\x7E]/u', '', $value);
95
    }
96
97
     /**
98
     * This function is used to hidden some part of the given string. Helpful
99
     * if you need hide some confidential information
100
     * like credit card number, password, etc.
101
     *
102
     * @param string $str the string you want to hide some part
103
     * @param int $startCount the length of non hidden for the beginning char
104
     * @param int $endCount the length of non hidden for the ending char
105
     * @param string $hiddenChar the char used to hide the given string
106
     *
107
     * @return string
108
     */
109
    public static function hidden(
110
        string $str,
111
        int $startCount = 0,
112
        int $endCount = 0,
113
        string $hiddenChar = '*'
114
    ): string {
115
        // get the string length
116
        $len = self::length($str);
117
        //if string is empty
118
        if ($len <= 0) {
119
            return self::repeat($hiddenChar, 6);
120
        }
121
122
        /*
123
         * if the length is less than $startCount and $endCount
124
         * or the $startCount and $endCount length is 0
125
         * or $startCount is negative or $endCount is negative
126
         * return the full string as hidden
127
         */
128
129
        if (
130
            (($startCount + $endCount) > $len) ||
131
            ($startCount == 0 && $endCount == 0) ||
132
            ($startCount < 0 || $endCount < 0)
133
        ) {
134
            return self::repeat($hiddenChar, $len);
135
        }
136
137
        // the start non hidden string
138
        $startNonHiddenStr = substr($str, 0, $startCount);
139
        // the end non hidden string
140
        $endNonHiddenStr = '';
141
        if ($endCount > 0) {
142
            $endNonHiddenStr = substr($str, - $endCount);
143
        }
144
        // the hidden string
145
        $hiddenStr = self::repeat($hiddenChar, $len - ($startCount + $endCount));
146
147
        return sprintf(
148
            '%s%s%s',
149
            $startNonHiddenStr,
150
            $hiddenStr,
151
            $endNonHiddenStr
152
        );
153
    }
154
155
    /**
156
     * Convert to camel case
157
     * @param string $value
158
     * @param bool $lcfirst
159
     * @return string
160
     */
161
    public static function camel(string $value, bool $lcfirst = true): string
162
    {
163
        if (isset(self::$camelCache[$value])) {
164
            return self::$camelCache[$value];
165
        }
166
167
        $studly = static::studly($value);
168
        return self::$camelCache[$value] = ($lcfirst ? lcfirst($studly) : $studly);
169
    }
170
171
    /**
172
     * Convert an string to array
173
     * @param string $value
174
     * @param non-empty-string $delimiter
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
175
     * @param int $limit
176
     * @return array<string>
177
     */
178
    public static function toArray(
179
        string $value,
180
        string $delimiter = ', ',
181
        int $limit = 0
182
    ): array {
183
        $string = trim($value, $delimiter . ' ');
184
        if ($string === '') {
185
            return [];
186
        }
187
188
        $values = [];
189
        /** @var array<string> $rawList */
190
        $rawList = $limit < 1
191
                ? (array) explode($delimiter, $string)
192
                : (array) explode($delimiter, $string, $limit);
193
194
        foreach ($rawList as $val) {
195
            $val = trim($val);
196
            if ($val !== '') {
197
                $values[] = $val;
198
            }
199
        }
200
201
        return $values;
202
    }
203
204
    /**
205
     * Determine if a given string contains a given sub string.
206
     * @param string $value
207
     * @param string|array<mixed> $needles
208
     * @return bool
209
     */
210
    public static function contains(string $value, string|array $needles): bool
211
    {
212
        if (!is_array($needles)) {
0 ignored issues
show
introduced by
The condition is_array($needles) is always true.
Loading history...
213
            $needles = [$needles];
214
        }
215
216
        foreach ($needles as $needle) {
217
            if ($needle !== '' && strpos($needle, $value) !== false) {
218
                return true;
219
            }
220
        }
221
222
        return false;
223
    }
224
225
    /**
226
     * Determine if a given string ends with a given sub string.
227
     * @param string $value
228
     * @param string|array<mixed> $needles
229
     * @return bool
230
     */
231
    public static function endsWith(string $value, string|array $needles): bool
232
    {
233
        if (!is_array($needles)) {
0 ignored issues
show
introduced by
The condition is_array($needles) is always true.
Loading history...
234
            $needles = [$needles];
235
        }
236
237
        foreach ($needles as $needle) {
238
            if ($value === (string) substr($needle, -strlen($value))) {
239
                return true;
240
            }
241
        }
242
243
        return false;
244
    }
245
246
    /**
247
     * Determine if a given string starts with a given sub string.
248
     * @param string $value
249
     * @param string|array<mixed> $needles
250
     * @return bool
251
     */
252
    public static function startsWith(string $value, string|array $needles): bool
253
    {
254
        if (!is_array($needles)) {
0 ignored issues
show
introduced by
The condition is_array($needles) is always true.
Loading history...
255
            $needles = [$needles];
256
        }
257
258
        foreach ($needles as $needle) {
259
            if ($needle !== '' && strpos($needle, $value) === 0) {
260
                return true;
261
            }
262
        }
263
264
        return false;
265
    }
266
267
    /**
268
     * Return the first line of multi line string
269
     * @param string $value
270
     * @return string
271
     */
272
    public static function firstLine(string $value): string
273
    {
274
        $str = trim($value);
275
276
        if ($str === '') {
277
            return '';
278
        }
279
280
        if (strpos($str, "\n") > 0) {
281
            $parts = explode("\n", $str);
282
283
            return $parts[0];
284
        }
285
286
        return $str;
287
    }
288
289
    /**
290
     * Cap a string with a single instance of a given value.
291
     * @param string $value
292
     * @param string $cap
293
     * @return string
294
     */
295
    public static function finish(string $value, string $cap): string
296
    {
297
        $quoted = preg_quote($cap, '/');
298
299
        return (string) preg_replace('/(?:' . $quoted . ')+$/', '', $value)
300
                . $cap;
301
    }
302
303
    /**
304
     * Determine if a given string matches a given pattern.
305
     * @param string $pattern
306
     * @param string $value
307
     * @return bool
308
     */
309
    public static function is(string $pattern, string $value): bool
310
    {
311
        if ($pattern === $value) {
312
            return true;
313
        }
314
315
        $quoted = preg_quote($pattern, '#');
316
317
        // Asterisks are translated into zero-or-more regular expression wildcards
318
        // to make it convenient to check if the strings starts with the given
319
        // pattern such as "library/*", making any string check convenient.
320
        $cleanQuoted = str_replace('\*', '.*', $quoted);
321
322
        return (bool)preg_match('#^' . $cleanQuoted . '\z#', $value);
323
    }
324
325
    /**
326
     * Return the length of the given string
327
     * @param string|int $value
328
     * @param string $encode
329
     * @return int
330
     */
331
    public static function length(string|int $value, string $encode = 'UTF-8'): int
332
    {
333
        if (!is_string($value)) {
334
            $value = (string) $value;
335
        }
336
337
        $length = mb_strlen($value, $encode);
338
339
        return $length;
340
    }
341
342
343
    /**
344
     * Add padding to string
345
     * @param string|int $value
346
     * @param int $length
347
     * @param string $padStr
348
     * @param int $type
349
     * @return string
350
     */
351
    public static function pad(
352
        string|int $value,
353
        int $length,
354
        string $padStr = ' ',
355
        int $type = STR_PAD_BOTH
356
    ): string {
357
        if (!is_string($value)) {
358
            $value = (string) $value;
359
        }
360
361
        return $length > 0
362
                ? str_pad($value, $length, $padStr, $type)
363
                : $value;
364
    }
365
366
    /**
367
     * Add padding to string to left
368
     * @param string|int $value
369
     * @param int $length
370
     * @param string $padStr
371
     * @return string
372
     */
373
    public static function padLeft(
374
        string|int $value,
375
        int $length,
376
        string $padStr = ' '
377
    ): string {
378
        return self::pad($value, $length, $padStr, STR_PAD_LEFT);
379
    }
380
381
    /**
382
     * Add padding to string to right
383
     * @param string|int $value
384
     * @param int $length
385
     * @param string $padStr
386
     * @return string
387
     */
388
    public static function padRight(
389
        string|int $value,
390
        int $length,
391
        string $padStr = ' '
392
    ): string {
393
        return self::pad($value, $length, $padStr, STR_PAD_RIGHT);
394
    }
395
396
    /**
397
     * Repeat the given string $length times
398
     * @param string|int $value
399
     * @param int $length
400
     * @return string
401
     */
402
    public static function repeat(string|int $value, int $length = 1): string
403
    {
404
        if (!is_string($value)) {
405
            $value = (string) $value;
406
        }
407
408
        return str_repeat($value, $length);
409
    }
410
411
    /**
412
     * Limit the length of given string
413
     * @param string $value
414
     * @param int $length
415
     * @param string $end
416
     * @return string
417
     */
418
    public static function limit(string $value, int $length = 100, string $end = '...'): string
419
    {
420
        if (mb_strwidth($value, 'UTF-8') <= $length) {
421
            return $value;
422
        }
423
424
        return rtrim(mb_strimwidth($value, 0, $length, '', 'UTF-8')) . $end;
425
    }
426
427
    /**
428
     * Limit the number of words in a string.
429
     * @param string $value
430
     * @param int $length
431
     * @param string $end
432
     * @return string
433
     */
434
    public static function words(string $value, int $length = 100, string $end = '...'): string
435
    {
436
        $matches = [];
437
        preg_match('/^\s*+(?:\S++\s*+){1,' . $length . '}/u', $value, $matches);
438
439
        if (!isset($matches[0]) || strlen($value) === strlen($matches[0])) {
440
            return $value;
441
        }
442
443
        return rtrim($matches[0]) . $end;
444
    }
445
446
    /**
447
     * Replace the first match of the given string
448
     * @param string $search
449
     * @param string $replace
450
     * @param string $value
451
     * @return string
452
     */
453
    public static function replaceFirst(string $search, string $replace, string $value): string
454
    {
455
        $pos = strpos($value, $search);
456
        if ($pos !== false) {
457
            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...
458
        }
459
460
        return $value;
461
    }
462
463
    /**
464
     * Replace the last match of the given string
465
     * @param string $search
466
     * @param string $replace
467
     * @param string $value
468
     * @return string
469
     */
470
    public static function replaceLast(string $search, string $replace, string $value): string
471
    {
472
        $pos = strrpos($value, $search);
473
474
        if ($pos !== false) {
475
            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...
476
        }
477
478
        return $value;
479
    }
480
481
    /**
482
     * Put the string to title format
483
     * @param string $value
484
     * @return string
485
     */
486
    public static function title(string $value): string
487
    {
488
        return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
489
    }
490
491
    /**
492
     * Generate a friendly "slug" from a given string.
493
     * @param string $value
494
     * @param string $separator
495
     * @return string
496
     */
497
    public static function slug(string $value, string $separator = '-'): string
498
    {
499
        $title = self::toAscii($value);
500
501
        // Convert all dashes/underscores into separator
502
        $flip = $separator === '-' ? '_' : '-';
503
504
        $utf8 = (string) preg_replace('![' . preg_quote($flip) . ']+!u', $separator, $title);
505
506
        // Remove all characters that are not the separator, letters, numbers,
507
        // or whitespace.
508
        $alphaNum = (string) preg_replace(
509
            '![^' . preg_quote($separator) . '\pL\pN\s]+!u',
510
            '',
511
            mb_strtolower($utf8)
512
        );
513
514
        // Replace all separator characters and whitespace by a single separator
515
        $removeWhitespace = (string) preg_replace(
516
            '![' . preg_quote($separator) . '\s]+!u',
517
            $separator,
518
            $alphaNum
519
        );
520
521
        return trim($removeWhitespace, $separator);
522
    }
523
524
    /**
525
     * Convert a string to snake case.
526
     * @param string $value
527
     * @param string $separator
528
     * @return string
529
     */
530
    public static function snake(string $value, string $separator = '_'): string
531
    {
532
        $key = $value . $separator;
533
        if (isset(self::$snakeCache[$key])) {
534
            return self::$snakeCache[$key];
535
        }
536
537
        if (!ctype_lower($value)) {
538
            $replace = (string) preg_replace('/\s+/', '', $value);
539
540
            $value = strtolower((string) preg_replace(
541
                '/(.)(?=[A-Z])/',
542
                '$1' . $separator,
543
                $replace
544
            ));
545
        }
546
547
        return self::$snakeCache[$key] = $value;
548
    }
549
550
    /**
551
     * Convert a value to studly caps case.
552
     * @param string $value
553
     * @return string
554
     */
555
    public static function studly(string $value): string
556
    {
557
        $key = $value;
558
        if (isset(self::$studlyCache[$key])) {
559
            return self::$studlyCache[$key];
560
        }
561
562
        $val = ucwords(str_replace(['-', '_'], ' ', $value));
563
564
        return self::$studlyCache[$key] = str_replace(' ', '', $val);
565
    }
566
567
    /**
568
     * Returns the portion of string specified by the start and
569
     * length parameters.
570
     *
571
     * @param string $value
572
     * @param int $start
573
     * @param int|null $length
574
     * @return string
575
     */
576
    public static function substr(string $value, int $start = 0, ?int $length = null): string
577
    {
578
        return mb_substr($value, $start, $length, 'UTF-8');
579
    }
580
581
    /**
582
     * Make a string's first character to upper case.
583
     * @param string $value
584
     * @return string
585
     */
586
    public static function ucfirst(string $value): string
587
    {
588
        return static::upper(
589
            static::substr($value, 0, 1)
590
        ) . static::substr($value, 1);
591
    }
592
593
    /**
594
     * Split the string by length part
595
     * @param string $value
596
     * @param int $length
597
     * @return array<int, string>
598
     */
599
    public static function split(string $value, int $length = 1): array
600
    {
601
        if ($length < 1) {
602
            return [];
603
        }
604
605
        if (self::isAscii($value)) {
606
            $res = str_split($value, $length);
607
            if ($res === false) {
608
                return [];
609
            }
610
611
            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...
612
        }
613
614
        if (mb_strlen($value) <= $length) {
615
            return [$value];
616
        }
617
        $matches = [];
618
        preg_match_all(
619
            '/.{' . $length . '}|[^\x00]{1,' . $length . '}$/us',
620
            $value,
621
            $matches
622
        );
623
624
        return $matches[0];
625
    }
626
627
    /**
628
     * Check whether the given string contains only ASCII chars
629
     * @param string $value
630
     * @return bool
631
     */
632
    public static function isAscii(string $value): bool
633
    {
634
        return (bool)!preg_match('/[^\x00-\x7F]/S', $value);
635
    }
636
637
    /**
638
     * Put string to lower case
639
     * @param string $value
640
     * @return string
641
     */
642
    public static function lower(string $value): string
643
    {
644
        return mb_strtolower($value, 'UTF-8');
645
    }
646
647
    /**
648
     * Put string to upper case
649
     * @param string $value
650
     * @return string
651
     */
652
    public static function upper(string $value): string
653
    {
654
        return mb_strtoupper($value, 'UTF-8');
655
    }
656
657
    /**
658
     * Return the unique ID
659
     * @param int $length
660
     *
661
     * @return string
662
     */
663
    public static function uniqId(int $length = 13): string
664
    {
665
        $bytes = random_bytes((int) ceil($length / 2));
666
667
        return (string)substr(bin2hex($bytes), 0, $length);
668
    }
669
670
    /**
671
     * Put array to HTML attributes
672
     * @param array<string, mixed> $attributes
673
     * @return string
674
     */
675
    public static function toAttribute(array $attributes): string
676
    {
677
        if (count($attributes) === 0) {
678
            return '';
679
        }
680
681
        // handle boolean, array, & html special chars
682
        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

682
        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...
683
            $value = is_bool($value) ? $value ? 'true' : 'false' : $value;
684
            $value = is_array($value) ? implode(' ', $value) : $value;
685
            $value = trim($value);
686
            $value = htmlspecialchars($value);
687
        });
688
689
        // remove empty elements
690
        $emptyAttributes = array_filter($attributes, function ($value) {
691
            return strlen($value) > 0;
692
        });
693
694
        if (empty($emptyAttributes)) {
695
            return '';
696
        }
697
698
        $compiled = implode('="%s" ', array_keys($emptyAttributes)) . '="%s"';
699
700
        return vsprintf($compiled, array_values($emptyAttributes));
701
    }
702
703
    /**
704
     * Generate random string value
705
     * @param int $length
706
     * @return string
707
     */
708
    public static function random(int $length = 16): string
709
    {
710
        $string = '';
711
        while (($len = strlen($string)) < $length) {
712
            $size = $length - $len;
713
            $bytes = random_bytes($size);
714
715
            $string .= substr(
716
                str_replace(['/', '+', '='], '', base64_encode($bytes)),
717
                0,
718
                $size
719
            );
720
        }
721
722
        return $string;
723
    }
724
725
    /**
726
     * Generates a random string of a given type and length. Possible
727
     * values for the first argument ($type) are:
728
     *  - alnum    - alpha-numeric characters (including capitals)
729
     *  - alpha    - alphabetical characters (including capitals)
730
     *  - hexdec   - hexadecimal characters, 0-9 plus a-f
731
     *  - numeric  - digit characters, 0-9
732
     *  - nozero   - digit characters, 1-9
733
     *  - distinct - clearly distinct alpha-numeric characters.
734
     * @param string $type
735
     * @param int $length
736
     * @return string
737
     */
738
    public static function randomString(string $type = 'alnum', int $length = 8): string
739
    {
740
        $utf8 = false;
741
742
        switch ($type) {
743
            case 'alnum':
744
                $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
745
                break;
746
            case 'alpha':
747
                $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
748
                break;
749
            case 'lowalnum':
750
                $pool = '0123456789abcdefghijklmnopqrstuvwxyz';
751
                break;
752
            case 'hexdec':
753
                $pool = '0123456789abcdef';
754
                break;
755
            case 'numeric':
756
                $pool = '0123456789';
757
                break;
758
            case 'nozero':
759
                $pool = '123456789';
760
                break;
761
            case 'distinct':
762
                $pool = '2345679ACDEFHJKLMNPRSTUVWXYZ';
763
                break;
764
            default:
765
                $pool = (string)$type;
766
                $utf8 = !self::isAscii($pool);
767
                break;
768
        }
769
770
        // Split the pool into an array of characters
771
        $pool = $utf8 ? self::split($pool, 1) : str_split($pool, 1);
772
        // Largest pool key
773
        $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

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