Passed
Push — master ( e6d102...7dc300 )
by Eric
12:39
created

Strings::doesContain()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 4
nop 4
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Utility - Collection of various PHP utility functions.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @version   2.0.0
10
 * @copyright (C) 2017 - 2024 Eric Sizemore
11
 * @license   The MIT License (MIT)
12
 *
13
 * Copyright (C) 2017 - 2024 Eric Sizemore <https://www.secondversion.com>.
14
 *
15
 * Permission is hereby granted, free of charge, to any person obtaining a copy
16
 * of this software and associated documentation files (the "Software"), to
17
 * deal in the Software without restriction, including without limitation the
18
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
19
 * sell copies of the Software, and to permit persons to whom the Software is
20
 * furnished to do so, subject to the following conditions:
21
 *
22
 * The above copyright notice and this permission notice shall be included in
23
 * all copies or substantial portions of the Software.
24
 *
25
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31
 * THE SOFTWARE.
32
 */
33
34
namespace Esi\Utility;
35
36
// Exceptions
37
use InvalidArgumentException;
38
use Random\RandomException;
1 ignored issue
show
Bug introduced by
The type Random\RandomException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
39
use ValueError;
40
41
// Functions
42
use function mb_convert_case;
43
use function mb_substr;
44
use function strcmp;
45
use function mb_strpos;
46
use function str_starts_with;
47
use function str_ends_with;
48
use function str_contains;
49
use function mb_strlen;
50
use function str_replace;
51
use function preg_replace;
52
use function preg_quote;
53
use function trim;
54
use function random_bytes;
55
use function bin2hex;
56
use function filter_var;
57
use function json_decode;
58
use function json_last_error;
59
use function array_map;
60
use function ord;
61
use function implode;
62
use function str_split;
63
use function sprintf;
64
use function preg_replace_callback;
65
use function ltrim;
66
67
// Constants
68
use const MB_CASE_TITLE;
69
use const MB_CASE_LOWER;
70
use const MB_CASE_UPPER;
71
use const FILTER_VALIDATE_EMAIL;
72
use const JSON_ERROR_NONE;
73
74
/**
75
 * String utilities.
76
 */
77
final class Strings
78
{
79
    /**
80
     * Encoding to use for multibyte-based functions.
81
     *
82
     * @var  string  Encoding
83
     */
84
    private static string $encoding = 'UTF-8';
85
86
    /**
87
     * Returns current encoding.
88
     *
89
     */
90
    public static function getEncoding(): string
91
    {
92
        return self::$encoding;
93
    }
94
95
    /**
96
     * Sets the encoding to use for multibyte-based functions.
97
     *
98
     * @param  string  $newEncoding  Charset.
99
     * @param  bool    $iniUpdate    Update php.ini's default_charset?
100
     */
101
    public static function setEncoding(string $newEncoding = '', bool $iniUpdate = false): void
102
    {
103
        if ($newEncoding !== '') {
104
            self::$encoding = $newEncoding;
105
        }
106
107
        if ($iniUpdate) {
108
            Environment::iniSet('default_charset', self::$encoding);
109
            Environment::iniSet('internal_encoding', self::$encoding);
110
        }
111
    }
112
113
    /**
114
     * title()
115
     *
116
     * Convert the given string to title case.
117
     *
118
     * @param   string  $value  Value to convert.
119
     * @return  string
120
     */
121
    public static function title(string $value): string
122
    {
123
        return mb_convert_case($value, MB_CASE_TITLE, (self::$encoding ?? null));
124
    }
125
126
    /**
127
     * lower()
128
     *
129
     * Convert the given string to lower case.
130
     *
131
     * @param   string  $value  Value to convert.
132
     * @return  string
133
     */
134
    public static function lower(string $value): string
135
    {
136
        return mb_convert_case($value, MB_CASE_LOWER, (self::$encoding ?? null));
137
    }
138
139
    /**
140
     * upper()
141
     *
142
     * Convert the given string to upper case.
143
     *
144
     * @param   string  $value  Value to convert.
145
     * @return  string
146
     */
147
    public static function upper(string $value): string
148
    {
149
        return mb_convert_case($value, MB_CASE_UPPER, (self::$encoding ?? null));
150
    }
151
152
    /**
153
     * substr()
154
     *
155
     * Returns the portion of string specified by the start and length parameters.
156
     *
157
     * @param   string    $string  The input string.
158
     * @param   int       $start   Start position.
159
     * @param   int|null  $length  Characters from $start.
160
     * @return  string             Extracted part of the string.
161
     */
162
    public static function substr(string $string, int $start, ?int $length = null): string
163
    {
164
        return mb_substr($string, $start, $length, (self::$encoding ?? null));
165
    }
166
167
    /**
168
     * lcfirst()
169
     *
170
     * Convert the first character of a given string to lower case.
171
     *
172
     * @since   1.0.1
173
     *
174
     * @param   string  $string  The input string.
175
     * @return  string
176
     */
177
    public static function lcfirst(string $string): string
178
    {
179
        return self::lower(self::substr($string, 0, 1)) . self::substr($string, 1);
180
    }
181
182
    /**
183
     * ucfirst()
184
     *
185
     * Convert the first character of a given string to upper case.
186
     *
187
     * @since   1.0.1
188
     *
189
     * @param   string  $string  The input string.
190
     * @return  string
191
     */
192
    public static function ucfirst(string $string): string
193
    {
194
        return self::upper(self::substr($string, 0, 1)) . self::substr($string, 1);
195
    }
196
197
    /**
198
     * Compares multibyte input strings in a binary safe case-insensitive manner.
199
     *
200
     * @since  1.0.1
201
     *
202
     * @param  string  $str1  The first string.
203
     * @param  string  $str2  The second string.
204
     * @return int            Returns < 0 if $str1 is less than $str2; > 0 if $str1
205
     *                        is greater than $str2, and 0 if they are equal.
206
     */
207
    public static function strcasecmp(string $str1, string $str2): int
208
    {
209
        return strcmp(Strings::upper($str1), Strings::upper($str2));
210
    }
211
212
    /**
213
     * beginsWith()
214
     *
215
     * Determine if a string begins with another string.
216
     *
217
     * @param   string  $haystack     String to search in.
218
     * @param   string  $needle       String to check for.
219
     * @param   bool    $insensitive  True to do a case-insensitive search.
220
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
221
     * @return  bool
222
     */
223
    public static function beginsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
224
    {
225
        if ($insensitive) {
226
            $haystack = Strings::lower($haystack);
227
            $needle   = Strings::lower($needle);
228
        }
229
        return (
230
            $multibyte
231
            ? mb_strpos($haystack, $needle) === 0
232
            : str_starts_with($haystack, $needle)
233
        );
234
    }
235
236
    /**
237
     * endsWith()
238
     *
239
     * Determine if a string ends with another string.
240
     *
241
     * @param   string  $haystack     String to search in.
242
     * @param   string  $needle       String to check for.
243
     * @param   bool    $insensitive  True to do a case-insensitive search.
244
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
245
     * @return  bool
246
     */
247
    public static function endsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
248
    {
249
        if ($insensitive) {
250
            $haystack = Strings::lower($haystack);
251
            $needle   = Strings::lower($needle);
252
        }
253
        return (
254
            $multibyte
255
            ? Strings::substr($haystack, -Strings::length($needle)) === $needle
256
            : str_ends_with($haystack, $needle)
257
        );
258
    }
259
260
    /**
261
     * doesContain()
262
     *
263
     * Determine if a string exists within another string.
264
     *
265
     * @param   string  $haystack     String to search in.
266
     * @param   string  $needle       String to check for.
267
     * @param   bool    $insensitive  True to do a case-insensitive search.
268
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
269
     * @return  bool
270
     */
271
    public static function doesContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
272
    {
273
        if ($insensitive) {
274
            $haystack = Strings::lower($haystack);
275
            $needle   = Strings::lower($needle);
276
        }
277
        return (
278
            $multibyte
279
            ? mb_strpos($haystack, $needle) !== false
280
            : str_contains($haystack, $needle)
281
        );
282
    }
283
284
    /**
285
     * doesNotContain()
286
     *
287
     * Determine if a string does not exist within another string.
288
     *
289
     * @param   string  $haystack     String to search in.
290
     * @param   string  $needle       String to check for.
291
     * @param   bool    $insensitive  True to do a case-insensitive search.
292
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
293
     * @return  bool
294
     */
295
    public static function doesNotContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
296
    {
297
        return (!Strings::doesContain($haystack, $needle, $insensitive, $multibyte));
298
    }
299
300
    /**
301
     * length()
302
     *
303
     * Get string length.
304
     *
305
     * @param   string  $string      The string being checked for length.
306
     * @param   bool    $binarySafe  Forces '8bit' encoding so that the length check is binary safe.
307
     * @return  int
308
     */
309
    public static function length(string $string, bool $binarySafe = false): int
310
    {
311
        return mb_strlen($string, ($binarySafe ? '8bit' : self::$encoding));
312
    }
313
314
    /**
315
     * camelCase()
316
     *
317
     * Returns a camelCase version of the string.
318
     *
319
     * Shoutout to Daniel St. Jules (https://github.com/danielstjules/Stringy/) for
320
     * inspiration for this function. This function is based on Stringy/camelize().
321
     *
322
     * My changes were mainly to make it fit into Utility.
323
     *
324
     * @since 2.0.0
325
     *
326
     * @param  string  $string  String to camelCase.
327
     * @return string           camelCase'd string.
328
     */
329
    public static function camelCase(string $string): string
330
    {
331
        // Trim surrounding spaces
332
        $string = trim($string);
333
        $string = self::lcfirst($string);
334
335
        // Remove leading dashes and underscores
336
        $string = ltrim($string, '-_');
337
338
        // Transformation
339
        $transformation = (string) preg_replace_callback(
340
            '/[-_\s]+(.)?/u',
341
            static fn (array $match): string => isset($match[1]) ? self::upper($match[1]) : '',
342
            $string
343
        );
344
        return (string) preg_replace_callback('/\p{N}+(.)?/u', static fn (array $match): string => self::upper($match[0]), $transformation);
345
    }
346
347
    /**
348
     * ascii()
349
     *
350
     * Transliterate a UTF-8 value to ASCII.
351
     *
352
     * Note: Adapted from Illuminate/Support/Str.
353
     *
354
     * @see https://packagist.org/packages/laravel/lumen-framework < v5.5
355
     * @see http://opensource.org/licenses/MIT
356
     *
357
     * @param   string  $value  Value to transliterate.
358
     * @return  string
359
     */
360
    public static function ascii(string $value): string
361
    {
362
        foreach (Strings::charMap() as $key => $val) {
363
            $value = str_replace($key, $val, $value);
364
        }
365
        return (string) preg_replace('/[^\x20-\x7E]/u', '', $value);
366
    }
367
368
    /**
369
     * charMap()
370
     *
371
     * Returns the replacements for the ascii method.
372
     *
373
     * @return  array<string, string>
374
     */
375
    private static function charMap(): array
376
    {
377
        static $charMap;
378
379
        return $charMap ??= [
380
            'Ǎ' => 'A', 'А' => 'A', 'Ā' => 'A', 'Ă' => 'A', 'Ą' => 'A', 'Å' => 'A',
381
            'Ǻ' => 'A', 'Ä' => 'Ae', 'Á' => 'A', 'À' => 'A', 'Ã' => 'A', 'Â' => 'A',
382
            'Æ' => 'AE', 'Ǽ' => 'AE', 'Б' => 'B', 'Ç' => 'C', 'Ć' => 'C', 'Ĉ' => 'C',
383
            'Č' => 'C', 'Ċ' => 'C', 'Ц' => 'C', 'Ч' => 'Ch', 'Ð' => 'Dj', 'Đ' => 'Dj',
384
            'Ď' => 'Dj', 'Д' => 'Dj', 'É' => 'E', 'Ę' => 'E', 'Ё' => 'E', 'Ė' => 'E',
385
            'Ê' => 'E', 'Ě' => 'E', 'Ē' => 'E', 'È' => 'E', 'Е' => 'E', 'Э' => 'E',
386
            'Ë' => 'E', 'Ĕ' => 'E', 'Ф' => 'F', 'Г' => 'G', 'Ģ' => 'G', 'Ġ' => 'G',
387
            'Ĝ' => 'G', 'Ğ' => 'G', 'Х' => 'H', 'Ĥ' => 'H', 'Ħ' => 'H', 'Ï' => 'I',
388
            'Ĭ' => 'I', 'İ' => 'I', 'Į' => 'I', 'Ī' => 'I', 'Í' => 'I', 'Ì' => 'I',
389
            'И' => 'I', 'Ǐ' => 'I', 'Ĩ' => 'I', 'Î' => 'I', 'IJ' => 'IJ', 'Ĵ' => 'J',
390
            'Й' => 'J', 'Я' => 'Ja', 'Ю' => 'Ju', 'К' => 'K', 'Ķ' => 'K', 'Ĺ' => 'L',
391
            'Л' => 'L', 'Ł' => 'L', 'Ŀ' => 'L', 'Ļ' => 'L', 'Ľ' => 'L', 'М' => 'M',
392
            'Н' => 'N', 'Ń' => 'N', 'Ñ' => 'N', 'Ņ' => 'N', 'Ň' => 'N', 'Ō' => 'O',
393
            'О' => 'O', 'Ǿ' => 'O', 'Ǒ' => 'O', 'Ơ' => 'O', 'Ŏ' => 'O', 'Ő' => 'O',
394
            'Ø' => 'O', 'Ö' => 'Oe', 'Õ' => 'O', 'Ó' => 'O', 'Ò' => 'O', 'Ô' => 'O',
395
            'Œ' => 'OE', 'П' => 'P', 'Ŗ' => 'R', 'Р' => 'R', 'Ř' => 'R', 'Ŕ' => 'R',
396
            'Ŝ' => 'S', 'Ş' => 'S', 'Š' => 'S', 'Ș' => 'S', 'Ś' => 'S', 'С' => 'S',
397
            'Ш' => 'Sh', 'Щ' => 'Shch', 'Ť' => 'T', 'Ŧ' => 'T', 'Ţ' => 'T', 'Ț' => 'T',
398
            'Т' => 'T', 'Ů' => 'U', 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U',
399
            'Ū' => 'U', 'Ǜ' => 'U', 'Ǚ' => 'U', 'Ù' => 'U', 'Ú' => 'U', 'Ü' => 'Ue',
400
            'Ǘ' => 'U', 'Ǖ' => 'U', 'У' => 'U', 'Ư' => 'U', 'Ǔ' => 'U', 'Û' => 'U',
401
            'В' => 'V', 'Ŵ' => 'W', 'Ы' => 'Y', 'Ŷ' => 'Y', 'Ý' => 'Y', 'Ÿ' => 'Y',
402
            'Ź' => 'Z', 'З' => 'Z', 'Ż' => 'Z', 'Ž' => 'Z', 'Ж' => 'Zh', 'á' => 'a',
403
            'ă' => 'a', 'â' => 'a', 'à' => 'a', 'ā' => 'a', 'ǻ' => 'a', 'å' => 'a',
404
            'ä' => 'ae', 'ą' => 'a', 'ǎ' => 'a', 'ã' => 'a', 'а' => 'a', 'ª' => 'a',
405
            'æ' => 'ae', 'ǽ' => 'ae', 'б' => 'b', 'č' => 'c', 'ç' => 'c', 'ц' => 'c',
406
            'ċ' => 'c', 'ĉ' => 'c', 'ć' => 'c', 'ч' => 'ch', 'ð' => 'dj', 'ď' => 'dj',
407
            'д' => 'dj', 'đ' => 'dj', 'э' => 'e', 'é' => 'e', 'ё' => 'e', 'ë' => 'e',
408
            'ê' => 'e', 'е' => 'e', 'ĕ' => 'e', 'è' => 'e', 'ę' => 'e', 'ě' => 'e',
409
            'ė' => 'e', 'ē' => 'e', 'ƒ' => 'f', 'ф' => 'f', 'ġ' => 'g', 'ĝ' => 'g',
410
            'ğ' => 'g', 'г' => 'g', 'ģ' => 'g', 'х' => 'h', 'ĥ' => 'h', 'ħ' => 'h',
411
            'ǐ' => 'i', 'ĭ' => 'i', 'и' => 'i', 'ī' => 'i', 'ĩ' => 'i', 'į' => 'i',
412
            'ı' => 'i', 'ì' => 'i', 'î' => 'i', 'í' => 'i', 'ï' => 'i', 'ij' => 'ij',
413
            'ĵ' => 'j', 'й' => 'j', 'я' => 'ja', 'ю' => 'ju', 'ķ' => 'k', 'к' => 'k',
414
            'ľ' => 'l', 'ł' => 'l', 'ŀ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'л' => 'l',
415
            'м' => 'm', 'ņ' => 'n', 'ñ' => 'n', 'ń' => 'n', 'н' => 'n', 'ň' => 'n',
416
            'ʼn' => 'n', 'ó' => 'o', 'ò' => 'o', 'ǒ' => 'o', 'ő' => 'o', 'о' => 'o',
417
            'ō' => 'o', 'º' => 'o', 'ơ' => 'o', 'ŏ' => 'o', 'ô' => 'o', 'ö' => 'oe',
418
            'õ' => 'o', 'ø' => 'o', 'ǿ' => 'o', 'œ' => 'oe', 'п' => 'p', 'р' => 'r',
419
            'ř' => 'r', 'ŕ' => 'r', 'ŗ' => 'r', 'ſ' => 's', 'ŝ' => 's', 'ș' => 's',
420
            'š' => 's', 'ś' => 's', 'с' => 's', 'ş' => 's', 'ш' => 'sh', 'щ' => 'shch',
421
            'ß' => 'ss', 'ţ' => 't', 'т' => 't', 'ŧ' => 't', 'ť' => 't', 'ț' => 't',
422
            'у' => 'u', 'ǘ' => 'u', 'ŭ' => 'u', 'û' => 'u', 'ú' => 'u', 'ų' => 'u',
423
            'ù' => 'u', 'ű' => 'u', 'ů' => 'u', 'ư' => 'u', 'ū' => 'u', 'ǚ' => 'u',
424
            'ǜ' => 'u', 'ǔ' => 'u', 'ǖ' => 'u', 'ũ' => 'u', 'ü' => 'ue', 'в' => 'v',
425
            'ŵ' => 'w', 'ы' => 'y', 'ÿ' => 'y', 'ý' => 'y', 'ŷ' => 'y', 'ź' => 'z',
426
            'ž' => 'z', 'з' => 'z', 'ż' => 'z', 'ж' => 'zh', 'ь' => '', 'ъ' => '',
427
            "\xC2\xA0"     => ' ', "\xE2\x80\x80" => ' ', "\xE2\x80\x81" => ' ',
428
            "\xE2\x80\x82" => ' ', "\xE2\x80\x83" => ' ', "\xE2\x80\x84" => ' ',
429
            "\xE2\x80\x85" => ' ', "\xE2\x80\x86" => ' ', "\xE2\x80\x87" => ' ',
430
            "\xE2\x80\x88" => ' ', "\xE2\x80\x89" => ' ', "\xE2\x80\x8A" => ' ',
431
            "\xE2\x80\xAF" => ' ', "\xE2\x81\x9F" => ' ', "\xE3\x80\x80" => ' ',
432
        ];
433
    }
434
435
    /**
436
     * slugify()
437
     *
438
     * Transforms a string into a URL or filesystem-friendly string.
439
     *
440
     * Note: Adapted from Illuminate/Support/Str::slug.
441
     *
442
     * @see https://packagist.org/packages/laravel/lumen-framework  < v5.5
443
     * @see http://opensource.org/licenses/MIT
444
     *
445
     * @param   string  $title      String to convert.
446
     * @param   string  $separator  Separator used to separate words in $title.
447
     * @return  string
448
     */
449
    public static function slugify(string $title, string $separator = '-'): string
450
    {
451
        $title = Strings::ascii($title);
452
453
        // Replace @ with the word 'at'
454
        $title = str_replace('@', $separator . 'at' . $separator, $title);
455
456
        $title = (string) preg_replace('![' . preg_quote(($separator === '-' ? '_' : '-')) . ']+!u', $separator, $title);
457
458
        // Remove all characters that are not the separator, letters, numbers, or whitespace.
459
        $title = (string) preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', Strings::lower($title));
460
461
        // Replace all separator characters and whitespace by a single separator
462
        $title = (string) preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title);
463
464
        // Cleanup $title
465
        return trim($title, $separator);
466
    }
467
468
    /**
469
     * randomBytes()
470
     *
471
     * Generate cryptographically secure pseudo-random bytes.
472
     *
473
     * @param   int<1, max>  $length  Length of the random string that should be returned in bytes.
474
     * @return  string
475
     *
476
     * @throws RandomException
477
     * @throws ValueError
478
     */
479
    public static function randomBytes(int $length): string
480
    {
481
        // Sanity check
482
        if ($length < 1) { // @phpstan-ignore-line
483
            throw new RandomException('$length must be greater than 1.');
484
        }
485
486
        // Generate bytes
487
        return random_bytes($length);
488
    }
489
490
    /**
491
     * randomString()
492
     *
493
     * Generates a secure random string, based on {@see static::randomBytes()}.
494
     *
495
     * @param   int<min, max>  $length  Length the random string should be.
496
     * @return  string
497
     *
498
     * @throws RandomException | ValueError
499
     */
500
    public static function randomString(int $length = 8): string
501
    {
502
        // Sanity check
503
        if ($length < 1) {
504
            throw new RandomException('$length must be greater than 1.');
505
        }
506
        // Convert bytes to hexadecimal and truncate to the desired length
507
        return Strings::substr(bin2hex(Strings::randomBytes($length * 2)), 0, $length);
508
    }
509
510
    /**
511
     * validEmail()
512
     *
513
     * Validate an email address using PHP's built-in filter.
514
     *
515
     * @param   string  $email Value to check.
516
     * @return  bool
517
     */
518
    public static function validEmail(string $email): bool
519
    {
520
        return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
521
    }
522
523
    /**
524
     * validJson()
525
     *
526
     * Determines if a string is valid JSON.
527
     *
528
     * @deprecated as of 2.0.0
529
     *
530
     * @param   string  $data   The string to validate as JSON.
531
     * @return  bool
532
     */
533
    public static function validJson(string $data): bool
534
    {
535
        $data = trim($data);
536
        json_decode($data);
537
538
        return (json_last_error() === JSON_ERROR_NONE);
539
    }
540
541
    /**
542
     * obscureEmail()
543
     *
544
     * Obscures an email address.
545
     *
546
     * @param   string  $email  Email address to obscure.
547
     * @return  string          Obscured email address.
548
     *
549
     * @throws  InvalidArgumentException
550
     */
551
    public static function obscureEmail(string $email): string
552
    {
553
        // Sanity check
554
        if (!Strings::validEmail($email)) {
555
            throw new InvalidArgumentException('Invalid $email specified.');
556
        }
557
558
        // Split and process
559
        $email = array_map(
560
            static fn (string $char): string => '&#' . ord($char) . ';',
561
            /** @scrutinizer ignore-type */ str_split($email)
562
        );
563
564
        return implode('', $email);
565
    }
566
567
    /**
568
     * guid()
569
     *
570
     * Generate a Globally/Universally Unique Identifier (version 4).
571
     *
572
     * @return  string
573
     *
574
     * @throws RandomException
575
     */
576
    public static function guid(): string
577
    {
578
        return sprintf(
579
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
580
            Numbers::random(0, 0xffff),
581
            Numbers::random(0, 0xffff),
582
            Numbers::random(0, 0xffff),
583
            Numbers::random(0, 0x0fff) | 0x4000,
584
            Numbers::random(0, 0x3fff) | 0x8000,
585
            Numbers::random(0, 0xffff),
586
            Numbers::random(0, 0xffff),
587
            Numbers::random(0, 0xffff)
588
        );
589
    }
590
}
591