Passed
Push — master ( 127279...062b2f )
by Eric
01:44
created

Utility::strcasecmp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
 * @package   Utility
10
 * @link      https://www.secondversion.com/
11
 * @version   1.2.0
12
 * @copyright (C) 2017 - 2023 Eric Sizemore
13
 * @license   The MIT License (MIT)
14
 */
15
namespace Esi\Utility;
16
17
// Exceptions
18
use Exception, InvalidArgumentException, RuntimeException, ValueError;
19
use FilesystemIterator, RecursiveDirectoryIterator, RecursiveIteratorIterator;
20
21
// Classes
22
use DateTime, DateTimeZone;
23
24
// Functions
25
use function abs, array_filter, array_keys, array_map, array_pop, array_merge_recursive;
26
use function bin2hex, call_user_func, ceil, chmod, count, date, trigger_error;
27
use function end, explode, fclose, file, file_get_contents, file_put_contents;
28
use function filter_var, floatval, fopen, function_exists, hash, header, headers_sent;
29
use function implode, in_array, inet_ntop, inet_pton, ini_get, ini_set, intval, is_array;
30
use function is_dir, is_file, is_null, is_readable, is_writable, json_decode, json_last_error;
31
use function mb_convert_case, mb_stripos, mb_strlen, mb_strpos, mb_substr, natsort;
32
use function number_format, ord, parse_url, preg_match, preg_quote, preg_replace;
33
use function random_bytes, random_int, rtrim, sprintf, str_replace, str_split;
34
use function strcmp, strtr, strval, time, trim, ucwords, unlink, str_contains, str_starts_with, str_ends_with;
35
36
// Constants
37
use const DIRECTORY_SEPARATOR, FILE_IGNORE_NEW_LINES, FILE_SKIP_EMPTY_LINES;
38
use const FILTER_FLAG_IPV4, FILTER_FLAG_IPV6, FILTER_FLAG_NO_PRIV_RANGE;
39
use const FILTER_FLAG_NO_RES_RANGE, FILTER_VALIDATE_EMAIL, FILTER_VALIDATE_IP;
40
use const JSON_ERROR_NONE, MB_CASE_LOWER, MB_CASE_TITLE, MB_CASE_UPPER;
41
use const PHP_INT_MAX, PHP_INT_MIN, PHP_SAPI, PHP_OS_FAMILY, E_USER_DEPRECATED;
42
43
/**
44
 * Utility - Collection of various PHP utility functions.
45
 *
46
 * @author    Eric Sizemore <[email protected]>
47
 * @package   Utility
48
 * @link      https://www.secondversion.com/
49
 * @version   1.2.0
50
 * @copyright (C) 2017 - 2023 Eric Sizemore
51
 * @license   The MIT License (MIT)
52
 *
53
 * Copyright (C) 2017 - 2023 Eric Sizemore. All rights reserved.
54
 *
55
 * Permission is hereby granted, free of charge, to any person obtaining a copy
56
 * of this software and associated documentation files (the "Software"), to
57
 * deal in the Software without restriction, including without limitation the
58
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
59
 * sell copies of the Software, and to permit persons to whom the Software is
60
 * furnished to do so, subject to the following conditions:
61
 *
62
 * The above copyright notice and this permission notice shall be included in
63
 * all copies or substantial portions of the Software.
64
 *
65
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
66
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
67
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
68
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
69
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
70
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
71
 * THE SOFTWARE.
72
 */
73
class Utility
74
{
75
    /**
76
     * Encoding to use for multibyte-based functions.
77
     *
78
     * @var  string  Encoding
79
     */
80
    private static string $encoding = 'UTF-8';
81
82
    /**
83
     * Returns current encoding.
84
     *
85
     * @return  string
86
     */
87 2
    public static function getEncoding(): string
88
    {
89 2
        return self::$encoding;
90
    }
91
92
    /**
93
     * Sets the encoding to use for multibyte-based functions.
94
     *
95
     * @param  string  $newEncoding  Charset
96
     * @param  bool    $iniUpdate    Update php.ini's default_charset?
97
     */
98 1
    public static function setEncoding(string $newEncoding = '', bool $iniUpdate = false): void
99
    {
100 1
        if ($newEncoding !== '') {
101 1
            self::$encoding = $newEncoding;
102
        }
103
104 1
        if ($iniUpdate) {
105 1
            static::iniSet('default_charset', self::$encoding);
106 1
            static::iniSet('internal_encoding', self::$encoding);
107
        }
108
    }
109
110
    /** array related functions **/
111
112
    /**
113
     * arrayFlatten()
114
     *
115
     * Flattens a multi-dimensional array.
116
     *
117
     * Keys are preserved based on $separator.
118
     *
119
     * @param   array<mixed>   $array      Array to flatten.
120
     * @param   string         $separator  The new keys are a list of original keys separated by $separator.
121
     *
122
     * @since 1.2.0
123
     * @param   string         $prepend    A string to prepend to resulting array keys.
124
     *
125
     * @return  array<mixed>               The flattened array.
126
     */
127 2
    public static function arrayFlatten(array $array, string $separator = '.', string $prepend = ''): array
128
    {
129 2
        $result = [];
130
131 2
        foreach ($array as $key => $value) {
132 2
            if (is_array($value) && $value !== []) {
133 2
                $result[] = static::arrayFlatten($value, $separator, $prepend . $key . $separator);
134
            } else {
135 2
                $result[] = [$prepend . $key => $value];
136
            }
137
        }
138
139 2
        if (count($result) === 0) {
140 1
            return [];
141
        }
142 2
        return array_merge_recursive([], ...$result);
143
    }
144
145
    /**
146
     * arrayMapDeep()
147
     *
148
     * Recursively applies a callback to all non-iterable elements of an array or an object.
149
     *
150
     * @since 1.2.0 - updated with inspiration from the WordPress map_deep() function.
151
     *      @see https://developer.wordpress.org/reference/functions/map_deep/
152
     *
153
     * @param   mixed     $array     The array to apply $callback to.
154
     * @param   callable  $callback  The callback function to apply.
155
     * @return  mixed
156
     */
157 2
    public static function arrayMapDeep(mixed $array, callable $callback): mixed
158
    {
159 2
        if (is_array($array)) {
160 2
            foreach ($array as $key => $value) {
161 2
                $array[$key] = static::arrayMapDeep($value, $callback);
162
            }
163 2
        } elseif (is_object($array)) {
164 1
            foreach (get_object_vars($array) as $key => $value) {
165 1
                $array->$key = static::arrayMapDeep($value, $callback);
166
            }
167
        } else {
168 2
            $array = call_user_func($callback, $array);
169
        }
170 2
        return $array;
171
    }
172
173
    /**
174
     * arrayInterlace()
175
     * 
176
     * Interlaces one or more arrays' values (not preserving keys).
177
     *
178
     * Example:
179
     *
180
     *      var_dump(Utility::arrayInterlace(
181
     *          [1, 2, 3],
182
     *          ['a', 'b', 'c']
183
     *      ));
184
     *
185
     * Result:
186
     *      Array (
187
     *          [0] => 1
188
     *          [1] => a
189
     *          [2] => 2
190
     *          [3] => b
191
     *          [4] => 3
192
     *          [5] => c
193
     *      )
194
     *
195
     * @since 1.2.0
196
     *
197
     * @param  array<mixed>        ...$args
198
     * @return array<mixed>|false
199
     */
200 1
    public static function arrayInterlace(array ...$args): array | false
201
    {
202 1
        $numArgs = count($args);
203
204 1
        if ($numArgs === 0) {
205 1
            return false;
206
        }
207
208 1
        if ($numArgs === 1) {
209 1
            return $args[0];
210
        }
211
212 1
        $totalElements = 0;
213
214 1
        for ($i = 0; $i < $numArgs; $i++) {
215 1
            $totalElements += count($args[$i]);
216
        }
217
218 1
        $newArray = [];
219
220 1
        for ($i = 0; $i < $totalElements; $i++) {
221 1
            for ($a = 0; $a < $numArgs; $a++) {
222 1
                if (isset($args[$a][$i])) {
223 1
                    $newArray[] = $args[$a][$i];
224
                }
225
            }
226
        }
227 1
        return $newArray;
228
    }
229
230
    /** string related functions **/
231
232
    /**
233
     * title()
234
     *
235
     * Convert the given string to title case.
236
     *
237
     * @param   string  $value  Value to convert.
238
     * @return  string
239
     */
240 1
    public static function title(string $value): string
241
    {
242 1
        return mb_convert_case($value, MB_CASE_TITLE, (self::$encoding ?? null));
243
    }
244
245
    /**
246
     * lower()
247
     *
248
     * Convert the given string to lower case.
249
     *
250
     * @param   string  $value  Value to convert.
251
     * @return  string
252
     */
253 10
    public static function lower(string $value): string
254
    {
255 10
        return mb_convert_case($value, MB_CASE_LOWER, (self::$encoding ?? null));
256
    }
257
258
    /**
259
     * upper()
260
     *
261
     * Convert the given string to upper case.
262
     *
263
     * @param   string  $value  Value to convert.
264
     * @return  string
265
     */
266 3
    public static function upper(string $value): string
267
    {
268 3
        return mb_convert_case($value, MB_CASE_UPPER, (self::$encoding ?? null));
269
    }
270
271
    /**
272
     * substr()
273
     *
274
     * Returns the portion of string specified by the start and length parameters.
275
     *
276
     * @param   string    $string  The input string.
277
     * @param   int       $start   Start position.
278
     * @param   int|null  $length  Characters from $start.
279
     * @return  string             Extracted part of the string.
280
     */
281 5
    public static function substr(string $string, int $start, ?int $length = null): string
282
    {
283 5
        return mb_substr($string, $start, $length, (self::$encoding ?? null));
284
    }
285
286
    /**
287
     * lcfirst()
288
     *
289
     * Convert the first character of a given string to lower case.
290
     *
291
     * @since   1.0.1
292
     *
293
     * @param   string  $string  The input string.
294
     * @return  string
295
     */
296 1
    public static function lcfirst(string $string): string
297
    {
298 1
        return self::lower(self::substr($string, 0, 1)) . self::substr($string, 1);
299
    }
300
301
    /**
302
     * ucfirst()
303
     *
304
     * Convert the first character of a given string to upper case.
305
     *
306
     * @since   1.0.1
307
     *
308
     * @param   string  $string  The input string.
309
     * @return  string
310
     */
311 1
    public static function ucfirst(string $string): string
312
    {
313 1
        return self::upper(self::substr($string, 0, 1)) . self::substr($string, 1);
314
    }
315
316
    /**
317
     * Compares multibyte input strings in a binary safe case-insensitive manner.
318
     *
319
     * @since  1.0.1
320
     *
321
     * @param  string  $str1  The first string.
322
     * @param  string  $str2  The second string.
323
     * @return int            Returns < 0 if $str1 is less than $str2; > 0 if $str1
324
     *                        is greater than $str2, and 0 if they are equal.
325
     */
326 1
    public static function strcasecmp(string $str1, string $str2): int
327
    {
328 1
        return strcmp(static::upper($str1), static::upper($str2));
329
    }
330
331
    /**
332
     * beginsWith()
333
     *
334
     * Determine if a string begins with another string.
335
     *
336
     * @param   string  $haystack     String to search in.
337
     * @param   string  $needle       String to check for.
338
     * @param   bool    $insensitive  True to do a case-insensitive search.
339
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
340
     * @return  bool
341
     */
342 1
    public static function beginsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
343
    {
344 1
        if ($multibyte === true) {
345 1
            if ($insensitive) {
346 1
                return mb_stripos($haystack, $needle) === 0;
347
            }
348
            return mb_strpos($haystack, $needle) === 0;
349
        }
350
351 1
        if ($insensitive) {
352 1
            $haystack = static::lower($haystack);
353 1
            $needle   = static::lower($needle);
354
        }
355 1
        return str_starts_with($haystack, $needle);
356
    }
357
358
    /**
359
     * endsWith()
360
     *
361
     * Determine if a string ends with another string.
362
     *
363
     * @param   string  $haystack     String to search in.
364
     * @param   string  $needle       String to check for.
365
     * @param   bool    $insensitive  True to do a case-insensitive search.
366
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
367
     * @return  bool
368
     */
369 1
    public static function endsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
370
    {
371 1
        if ($insensitive) {
372 1
            $haystack = static::lower($haystack);
373 1
            $needle   = static::lower($needle);
374
        }
375 1
        return (
376 1
            $multibyte
377 1
            ? static::substr($haystack, -static::length($needle)) === $needle
378 1
            : str_ends_with($haystack, $needle)
379 1
        );
380
    }
381
382
    /**
383
     * doesContain()
384
     *
385
     * Determine if a string exists within another string.
386
     *
387
     * @param   string  $haystack     String to search in.
388
     * @param   string  $needle       String to check for.
389
     * @param   bool    $insensitive  True to do a case-insensitive search.
390
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
391
     * @return  bool
392
     */
393 2
    public static function doesContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
394
    {
395 2
        if ($insensitive) {
396 1
            $haystack = static::lower($haystack);
397 1
            $needle   = static::lower($needle);
398
        }
399 2
        return (
400 2
            $multibyte
401 1
            ? mb_strpos($haystack, $needle) !== false
402 2
            : str_contains($haystack, $needle) !== false
403 2
        );
404
    }
405
406
    /**
407
     * doesNotContain()
408
     *
409
     * Determine if a string does not exist within another string.
410
     *
411
     * @param   string  $haystack     String to search in.
412
     * @param   string  $needle       String to check for.
413
     * @param   bool    $insensitive  True to do a case-insensitive search.
414
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
415
     * @return  bool
416
     */
417 5
    public static function doesNotContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
418
    {
419 5
        if ($insensitive) {
420 1
            $haystack = static::lower($haystack);
421 1
            $needle   = static::lower($needle);
422
        }
423 5
        return (
424 5
            $multibyte
425 1
            ? mb_strpos($haystack, $needle) === false
426 5
            : str_contains($haystack, $needle) === false
427 5
        );
428
    }
429
430
    /**
431
     * length()
432
     *
433
     * Get string length.
434
     *
435
     * @param   string  $string      The string being checked for length.
436
     * @param   bool    $binarySafe  Forces '8bit' encoding so that the length check is binary safe.
437
     * @return  int
438
     */
439 3
    public static function length(string $string, bool $binarySafe = false): int
440
    {
441 3
        return mb_strlen($string, ($binarySafe ? '8bit' : self::$encoding));
442
    }
443
444
    /**
445
     * ascii()
446
     *
447
     * Transliterate a UTF-8 value to ASCII.
448
     *
449
     * Note: Adapted from Illuminate/Support/Str
450
     *
451
     * @see https://packagist.org/packages/laravel/lumen-framework < v5.5
452
     * @see http://opensource.org/licenses/MIT
453
     *
454
     * @param   string  $value  Value to transliterate.
455
     * @return  string
456
     */
457 2
    public static function ascii(string $value): string
458
    {
459 2
        foreach (static::charMap() as $key => $val) {
460 2
            $value = str_replace($key, $val, $value);
461
        }
462
        // preg_replace can return null if it encounters an error, so we return
463
        // the passed $value in that instance.
464 2
        return preg_replace('/[^\x20-\x7E]/u', '', $value) ?? $value;
465
    }
466
467
    /**
468
     * charMap()
469
     *
470
     * Returns the replacements for the ascii method.
471
     *
472
     * @return  array<string, string>
473
     */
474 2
    protected static function charMap(): array
475
    {
476 2
        static $charMap;
477
478 2
        if (isset($charMap)) {
479 1
            return $charMap;
480
        }
481 1
        return $charMap = [
482 1
            'Ǎ' => 'A', 'А' => 'A', 'Ā' => 'A', 'Ă' => 'A', 'Ą' => 'A', 'Å' => 'A',
483 1
            'Ǻ' => 'A', 'Ä' => 'Ae', 'Á' => 'A', 'À' => 'A', 'Ã' => 'A', 'Â' => 'A',
484 1
            'Æ' => 'AE', 'Ǽ' => 'AE', 'Б' => 'B', 'Ç' => 'C', 'Ć' => 'C', 'Ĉ' => 'C',
485 1
            'Č' => 'C', 'Ċ' => 'C', 'Ц' => 'C', 'Ч' => 'Ch', 'Ð' => 'Dj', 'Đ' => 'Dj',
486 1
            'Ď' => 'Dj', 'Д' => 'Dj', 'É' => 'E', 'Ę' => 'E', 'Ё' => 'E', 'Ė' => 'E',
487 1
            'Ê' => 'E', 'Ě' => 'E', 'Ē' => 'E', 'È' => 'E', 'Е' => 'E', 'Э' => 'E',
488 1
            'Ë' => 'E', 'Ĕ' => 'E', 'Ф' => 'F', 'Г' => 'G', 'Ģ' => 'G', 'Ġ' => 'G',
489 1
            'Ĝ' => 'G', 'Ğ' => 'G', 'Х' => 'H', 'Ĥ' => 'H', 'Ħ' => 'H', 'Ï' => 'I',
490 1
            'Ĭ' => 'I', 'İ' => 'I', 'Į' => 'I', 'Ī' => 'I', 'Í' => 'I', 'Ì' => 'I',
491 1
            'И' => 'I', 'Ǐ' => 'I', 'Ĩ' => 'I', 'Î' => 'I', 'IJ' => 'IJ', 'Ĵ' => 'J',
492 1
            'Й' => 'J', 'Я' => 'Ja', 'Ю' => 'Ju', 'К' => 'K', 'Ķ' => 'K', 'Ĺ' => 'L',
493 1
            'Л' => 'L', 'Ł' => 'L', 'Ŀ' => 'L', 'Ļ' => 'L', 'Ľ' => 'L', 'М' => 'M',
494 1
            'Н' => 'N', 'Ń' => 'N', 'Ñ' => 'N', 'Ņ' => 'N', 'Ň' => 'N', 'Ō' => 'O',
495 1
            'О' => 'O', 'Ǿ' => 'O', 'Ǒ' => 'O', 'Ơ' => 'O', 'Ŏ' => 'O', 'Ő' => 'O',
496 1
            'Ø' => 'O', 'Ö' => 'Oe', 'Õ' => 'O', 'Ó' => 'O', 'Ò' => 'O', 'Ô' => 'O',
497 1
            'Œ' => 'OE', 'П' => 'P', 'Ŗ' => 'R', 'Р' => 'R', 'Ř' => 'R', 'Ŕ' => 'R',
498 1
            'Ŝ' => 'S', 'Ş' => 'S', 'Š' => 'S', 'Ș' => 'S', 'Ś' => 'S', 'С' => 'S',
499 1
            'Ш' => 'Sh', 'Щ' => 'Shch', 'Ť' => 'T', 'Ŧ' => 'T', 'Ţ' => 'T', 'Ț' => 'T',
500 1
            'Т' => 'T', 'Ů' => 'U', 'Ű' => 'U', 'Ŭ' => 'U', 'Ũ' => 'U', 'Ų' => 'U',
501 1
            'Ū' => 'U', 'Ǜ' => 'U', 'Ǚ' => 'U', 'Ù' => 'U', 'Ú' => 'U', 'Ü' => 'Ue',
502 1
            'Ǘ' => 'U', 'Ǖ' => 'U', 'У' => 'U', 'Ư' => 'U', 'Ǔ' => 'U', 'Û' => 'U',
503 1
            'В' => 'V', 'Ŵ' => 'W', 'Ы' => 'Y', 'Ŷ' => 'Y', 'Ý' => 'Y', 'Ÿ' => 'Y',
504 1
            'Ź' => 'Z', 'З' => 'Z', 'Ż' => 'Z', 'Ž' => 'Z', 'Ж' => 'Zh', 'á' => 'a',
505 1
            'ă' => 'a', 'â' => 'a', 'à' => 'a', 'ā' => 'a', 'ǻ' => 'a', 'å' => 'a',
506 1
            'ä' => 'ae', 'ą' => 'a', 'ǎ' => 'a', 'ã' => 'a', 'а' => 'a', 'ª' => 'a',
507 1
            'æ' => 'ae', 'ǽ' => 'ae', 'б' => 'b', 'č' => 'c', 'ç' => 'c', 'ц' => 'c',
508 1
            'ċ' => 'c', 'ĉ' => 'c', 'ć' => 'c', 'ч' => 'ch', 'ð' => 'dj', 'ď' => 'dj',
509 1
            'д' => 'dj', 'đ' => 'dj', 'э' => 'e', 'é' => 'e', 'ё' => 'e', 'ë' => 'e',
510 1
            'ê' => 'e', 'е' => 'e', 'ĕ' => 'e', 'è' => 'e', 'ę' => 'e', 'ě' => 'e',
511 1
            'ė' => 'e', 'ē' => 'e', 'ƒ' => 'f', 'ф' => 'f', 'ġ' => 'g', 'ĝ' => 'g',
512 1
            'ğ' => 'g', 'г' => 'g', 'ģ' => 'g', 'х' => 'h', 'ĥ' => 'h', 'ħ' => 'h',
513 1
            'ǐ' => 'i', 'ĭ' => 'i', 'и' => 'i', 'ī' => 'i', 'ĩ' => 'i', 'į' => 'i',
514 1
            'ı' => 'i', 'ì' => 'i', 'î' => 'i', 'í' => 'i', 'ï' => 'i', 'ij' => 'ij',
515 1
            'ĵ' => 'j', 'й' => 'j', 'я' => 'ja', 'ю' => 'ju', 'ķ' => 'k', 'к' => 'k',
516 1
            'ľ' => 'l', 'ł' => 'l', 'ŀ' => 'l', 'ĺ' => 'l', 'ļ' => 'l', 'л' => 'l',
517 1
            'м' => 'm', 'ņ' => 'n', 'ñ' => 'n', 'ń' => 'n', 'н' => 'n', 'ň' => 'n',
518 1
            'ʼn' => 'n', 'ó' => 'o', 'ò' => 'o', 'ǒ' => 'o', 'ő' => 'o', 'о' => 'o',
519 1
            'ō' => 'o', 'º' => 'o', 'ơ' => 'o', 'ŏ' => 'o', 'ô' => 'o', 'ö' => 'oe',
520 1
            'õ' => 'o', 'ø' => 'o', 'ǿ' => 'o', 'œ' => 'oe', 'п' => 'p', 'р' => 'r',
521 1
            'ř' => 'r', 'ŕ' => 'r', 'ŗ' => 'r', 'ſ' => 's', 'ŝ' => 's', 'ș' => 's',
522 1
            'š' => 's', 'ś' => 's', 'с' => 's', 'ş' => 's', 'ш' => 'sh', 'щ' => 'shch',
523 1
            'ß' => 'ss', 'ţ' => 't', 'т' => 't', 'ŧ' => 't', 'ť' => 't', 'ț' => 't',
524 1
            'у' => 'u', 'ǘ' => 'u', 'ŭ' => 'u', 'û' => 'u', 'ú' => 'u', 'ų' => 'u',
525 1
            'ù' => 'u', 'ű' => 'u', 'ů' => 'u', 'ư' => 'u', 'ū' => 'u', 'ǚ' => 'u',
526 1
            'ǜ' => 'u', 'ǔ' => 'u', 'ǖ' => 'u', 'ũ' => 'u', 'ü' => 'ue', 'в' => 'v',
527 1
            'ŵ' => 'w', 'ы' => 'y', 'ÿ' => 'y', 'ý' => 'y', 'ŷ' => 'y', 'ź' => 'z',
528 1
            'ž' => 'z', 'з' => 'z', 'ż' => 'z', 'ж' => 'zh', 'ь' => '', 'ъ' => '',
529 1
            "\xC2\xA0"     => ' ', "\xE2\x80\x80" => ' ', "\xE2\x80\x81" => ' ',
530 1
            "\xE2\x80\x82" => ' ', "\xE2\x80\x83" => ' ', "\xE2\x80\x84" => ' ',
531 1
            "\xE2\x80\x85" => ' ', "\xE2\x80\x86" => ' ', "\xE2\x80\x87" => ' ',
532 1
            "\xE2\x80\x88" => ' ', "\xE2\x80\x89" => ' ', "\xE2\x80\x8A" => ' ',
533 1
            "\xE2\x80\xAF" => ' ', "\xE2\x81\x9F" => ' ', "\xE3\x80\x80" => ' '
534
535 1
        ];
536
    }
537
538
    /**
539
     * slugify()
540
     *
541
     * Transforms a string into a URL or filesystem-friendly string.
542
     *
543
     * Note: Adapted from Illuminate/Support/Str::slug
544
     *
545
     * @see https://packagist.org/packages/laravel/lumen-framework  < v5.5
546
     * @see http://opensource.org/licenses/MIT
547
     *
548
     * @param   string  $title      String to convert.
549
     * @param   string  $separator  Separator used to separate words in $title.
550
     * @return  string
551
     */
552 1
    public static function slugify(string $title, string $separator = '-'): string
553
    {
554 1
        $title = static::ascii($title);
555
556
        // preg_replace can return null if an error occurs. It shouldn't happen, but if it does,
557
        // we return what we have processed thus far
558 1
        $title = (
559 1
            preg_replace('![' . preg_quote(($separator === '-' ? '_' : '-')) . ']+!u', $separator, $title)
560
            ?? $title
561 1
        );
562
563
        // Replace @ with the word 'at'
564 1
        $title = str_replace('@', $separator . 'at' . $separator, $title);
565
566
        // Remove all characters that are not the separator, letters, numbers, or whitespace.
567 1
        $title = (
568 1
            preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', static::lower($title))
569
            ?? $title
570 1
        );
571
572
        // Replace all separator characters and whitespace by a single separator
573 1
        $title = (
574 1
            preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title)
575
            ?? $title
576 1
        );
577
578
        // Cleanup $title
579 1
        return trim($title, $separator);
580
    }
581
582
    /**
583
     * randomBytes()
584
     *
585
     * Generate cryptographically secure pseudo-random bytes.
586
     *
587
     * @param   int     $length  Length of the random string that should be returned in bytes.
588
     * @return  string
589
     *
590
     * @throws \Random\RandomException If an invalid length is specified.
591
     *                                 If the random_bytes() function somehow fails.
592
     */
593 2
    public static function randomBytes(int $length): string
594
    {
595
        // Sanity check
596 2
        if ($length < 1 || $length > PHP_INT_MAX) {
597 1
            throw new \Random\RandomException('Invalid $length specified.');
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...
598
        }
599
600
        // Generate bytes
601
        try {
602 2
            $bytes = random_bytes($length);
603
        } catch (\Random\RandomException $e) {
604
            throw new \Random\RandomException(
605
                'Utility was unable to generate random bytes: ' . $e->getMessage(), $e->getCode(), $e->getPrevious()
606
            );
607
        }
608 2
        return $bytes;
609
    }
610
611
    /**
612
     * randomInt()
613
     *
614
     * Generate a cryptographically secure pseudo-random integer.
615
     *
616
     * @param   int  $min  The lowest value to be returned, which must be PHP_INT_MIN or higher.
617
     * @param   int  $max  The highest value to be returned, which must be less than or equal to PHP_INT_MAX.
618
     * @return  int
619
     *
620
     * @throws \Random\RandomException
621
     */
622 2
    public static function randomInt(int $min, int $max): int
623
    {
624
        // Sanity check
625 2
        if ($min < PHP_INT_MIN || $max > PHP_INT_MAX) {
626
            throw new \Random\RandomException('$min and $max values must be within the \PHP_INT_MIN, \PHP_INT_MAX range');
627
        }
628
629 2
        if ($min >= $max) {
630 1
            throw new \Random\RandomException('$min value must be less than $max.');
631
        }
632
633
        // Generate random int
634
        try {
635 2
            $int = random_int($min, $max);
636
        } catch (\Random\RandomException $e) {
637
            throw new \Random\RandomException(
638
                'Utility was unable to generate random int: ' . $e->getMessage(), $e->getCode(), $e->getPrevious()
639
            );
640
        }
641 2
        return $int;
642
    }
643
644
    /**
645
     * randomString()
646
     *
647
     * Generates a secure random string, based on {@see static::randomBytes()}.
648
     *
649
     * @todo A better implementation. Could be done better.
650
     *
651
     * @param   int     $length  Length the random string should be.
652
     * @return  string
653
     *
654
     * @throws \Random\RandomException
655
     */
656 1
    public static function randomString(int $length = 8): string
657
    {
658
        // Sanity check
659 1
        if ($length <= 0) {
660 1
            throw new \Random\RandomException('$length must be greater than 0.');
661
        }
662
663
        // Attempt to get random bytes
664
        try {
665 1
            $bytes = static::randomBytes($length * 2);
666
667 1
            if ($bytes === '') {
668 1
                throw new \Random\RandomException('Random bytes generator failure.');
669
            }
670
        } catch (\Random\RandomException $e) {
671
            throw new \Random\RandomException($e->getMessage(), 0, $e);
672
        }
673 1
        return static::substr(bin2hex($bytes), 0, $length);
674
    }
675
676
    /** directory/file related functions **/
677
678
    /**
679
     * lineCounter()
680
     *
681
     * Parse a project directory for approximate line count for a project's
682
     * codebase.
683
     *
684
     * @param   string          $directory      Directory to parse.
685
     * @param   array<string>   $ignore         Subdirectories of $directory you wish
686
     *                                          to not include in the line count.
687
     * @param   array<string>   $extensions     An array of file types/extensions of
688
     *                                          files you want included in the line count.
689
     * @param   bool            $skipEmpty      If set to true, will not include empty
690
     *                                          lines in the line count.
691
     * @param   bool            $onlyLineCount  If set to true, only returns an array
692
     *                                          of line counts without directory/filenames.
693
     * @return  array<mixed>
694
     *
695
     * @throws  InvalidArgumentException
696
     */
697 1
    public static function lineCounter(string $directory, array $ignore = [], array $extensions = [], bool $skipEmpty = true, bool $onlyLineCount = false): array
698
    {
699
        // Sanity check
700 1
        if (!is_dir($directory) || !is_readable($directory)) {
701 1
            throw new InvalidArgumentException('Invalid $directory specified');
702
        }
703
704
        // Initialize
705 1
        $lines = [];
706
707
        // Flags passed to \file().
708 1
        $flags = FILE_IGNORE_NEW_LINES;
709
710 1
        if ($skipEmpty) {
711 1
            $flags |= FILE_SKIP_EMPTY_LINES;
712
        }
713
714
        // Directory names we wish to ignore
715 1
        if (count($ignore) > 0) {
716 1
            $ignore = preg_quote(implode('|', $ignore), '#');
717
        }
718
719
        // Traverse the directory
720 1
        $iterator = new RecursiveIteratorIterator(
721 1
            new RecursiveDirectoryIterator(
722 1
                $directory,
723 1
                FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS
724 1
            )
725 1
        );
726
727
        // Build the actual contents of the directory
728
        /** @var RecursiveDirectoryIterator $val **/
729 1
        foreach ($iterator as $key => $val) {
730 1
            if ($val->isFile()) {
731
                if (
732 1
                    (is_string($ignore) && preg_match("#($ignore)#i", $val->getPath()) === 1)
733 1
                    || (count($extensions) > 0 && !in_array($val->getExtension(), $extensions, true))
734
                ) {
735 1
                    continue;
736
                }
737
738 1
                $content = file($val->getPath() . DIRECTORY_SEPARATOR . $val->getFilename(), $flags);
739
740 1
                if ($content === false) {
741
                    continue;
742
                }
743
                /** @var int<0, max> $content **/
744 1
                $content = count(/** @scrutinizer ignore-type */$content);
745
746 1
                $lines[$val->getPath()][$val->getFilename()] = $content;
747
            }
748
        }
749 1
        unset($iterator);
750
751 1
        return ($onlyLineCount ? static::arrayFlatten($lines) : $lines);
752
    }
753
754
    /**
755
     * directorySize()
756
     *
757
     * Retrieves size of a directory (in bytes).
758
     *
759
     * @param   string          $directory  Directory to parse.
760
     * @param   array<string>   $ignore     Subdirectories of $directory you wish to not include.
761
     * @return  int
762
     *
763
     * @throws  InvalidArgumentException
764
     */
765 1
    public static function directorySize(string $directory, array $ignore = []): int
766
    {
767
        // Sanity checks
768 1
        if (!is_dir($directory) || !is_readable($directory)) {
769 1
            throw new InvalidArgumentException('Invalid $directory specified');
770
        }
771
772
        // Initialize
773 1
        $size = 0;
774
775
        // Directories we wish to ignore, if any
776 1
        $ignore = (count($ignore) > 0) ? preg_quote(implode('|', $ignore), '#') : '';
777
778
        // Traverse the directory
779 1
        $iterator = new RecursiveIteratorIterator(
780 1
            new RecursiveDirectoryIterator(
781 1
                $directory,
782 1
                FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS
783 1
            )
784 1
        );
785
786
        // Determine directory size by checking file sizes
787
        /** @var RecursiveDirectoryIterator $val **/
788 1
        foreach ($iterator as $key => $val) {
789 1
            if ($ignore !== '' && preg_match("#($ignore)#i", $val->getPath()) === 1) {
790 1
                continue;
791
            }
792
793 1
            if ($val->isFile()) {
794 1
                $size += $val->getSize();
795
            }
796
        }
797 1
        unset($iterator);
798
799 1
        return $size;
800
    }
801
802
    /**
803
     * Retrieves contents of a directory.
804
     *
805
     * @param   string          $directory  Directory to parse.
806
     * @param   array<string>   $ignore     Subdirectories of $directory you wish to not include.
807
     * @return  array<mixed>
808
     *
809
     * @throws  InvalidArgumentException
810
     */
811 1
    public static function directoryList(string $directory, array $ignore = []): array
812
    {
813
        // Sanity checks
814 1
        if (!is_dir($directory) || !is_readable($directory)) {
815 1
            throw new InvalidArgumentException('Invalid $directory specified');
816
        }
817
818
        // Initialize
819 1
        $contents = [];
820
821
        // Directories to ignore, if any
822 1
        $ignore = (count($ignore) > 0) ? preg_quote(implode('|', $ignore), '#') : '';
823
824
        // Traverse the directory
825 1
        $iterator = new RecursiveIteratorIterator(
826 1
            new RecursiveDirectoryIterator(
827 1
                $directory,
828 1
                FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS
829 1
            )
830 1
        );
831
832
        // Build the actual contents of the directory
833
        /** @var RecursiveDirectoryIterator $val **/
834 1
        foreach ($iterator AS $key => $val) {
835 1
            if ($ignore !== '' && preg_match("#($ignore)#i", $val->getPath()) === 1) {
836 1
                continue;
837
            }
838 1
            $contents[] = $key;
839
        }
840 1
        natsort($contents);
841
842 1
        return $contents;
843
    }
844
845
    /**
846
     * normalizeFilePath()
847
     *
848
     * Normalizes a file or directory path.
849
     *
850
     * @param   string  $path       The file or directory path.
851
     * @param   string  $separator  The directory separator. Defaults to DIRECTORY_SEPARATOR.
852
     * @return  string              The normalized file or directory path
853
     */
854 1
    public static function normalizeFilePath(string $path, string $separator = DIRECTORY_SEPARATOR): string
855
    {
856
        // Clean up our path
857 1
        $separator = ($separator === '' ? DIRECTORY_SEPARATOR : $separator);
858
859 1
        $path = rtrim(strtr($path, '/\\', $separator . $separator), $separator);
860
861
        if (
862 1
            static::doesNotContain($separator . $path, "{$separator}.")
863 1
            && static::doesNotContain($path, $separator . $separator)
864
        ) {
865 1
            return $path;
866
        }
867
868
        // Initialize
869 1
        $parts = [];
870
871
        // Grab file path parts
872 1
        foreach (explode($separator, $path) as $part) {
873 1
            if ($part === '..' && count($parts) > 0 && end($parts) !== '..') {
874 1
                array_pop($parts);
875 1
            } elseif ($part === '.' || $part === '' && count($parts) > 0) {
876 1
                continue;
877
            } else {
878 1
                $parts[] = $part;
879
            }
880
        }
881
882
        // Build
883 1
        $path = implode($separator, $parts);
884 1
        return ($path === '' ? '.' : $path);
885
    }
886
887
    /**
888
     * isReallyWritable()
889
     *
890
     * Checks to see if a file or directory is really writable.
891
     *
892
     * @param   string  $file  File or directory to check.
893
     * @return  bool
894
     *
895
     * @throws \Random\RandomException  If unable to generate random string for the temp file
896
     * @throws RuntimeException         If the file or directory does not exist
897
     */
898 6
    public static function isReallyWritable(string $file): bool
899
    {
900 6
        clearstatcache();
901
902 6
        if (!file_exists($file)) {
903 1
            throw new RuntimeException('Invalid file or directory specified');
904
        }
905
906
        // If we are on Unix/Linux just run is_writable()
907 6
        if (PHP_OS_FAMILY !== 'Windows') {
908 6
            return is_writable($file);
909
        }
910
911
        // Otherwise, if on Windows...
912
        $tmpFile = rtrim($file, '\\/') . DIRECTORY_SEPARATOR . hash('md5', static::randomString()) . '.txt';
913
        $tmpData = 'tmpData';
914
915
        $directoryOrFile = (is_dir($file));
916
917
        if ($directoryOrFile) {
918
            $data = file_put_contents($tmpFile, $tmpData, FILE_APPEND);
919
        } else {
920
            $data = file_get_contents($file);
921
        }
922
923
        if (file_exists($tmpFile)) {
924
            unlink($tmpFile);
925
        }
926
        return ($data !== false ? true : false);
927
    }
928
929
    /**
930
     * fileRead()
931
     *
932
     * Perform a read operation on a pre-existing file.
933
     *
934
     * @param   string       $file  Filename
935
     * @return  string|false
936
     *
937
     * @throws  InvalidArgumentException
938
     */
939 1
    public static function fileRead(string $file): string | false
940
    {
941
        // Sanity check
942 1
        if (!is_readable($file)) {
943 1
            throw new InvalidArgumentException(sprintf("File '%s' does not exist or is not readable.", $file));
944
        }
945 1
        return file_get_contents($file);
946
    }
947
948
    /**
949
     * fileWrite()
950
     *
951
     * Perform a write operation on a pre-existing file.
952
     *
953
     * @param   string  $file   Filename
954
     * @param   string  $data   If writing to the file, the data to write.
955
     * @param   int     $flags  Bitwise OR'ed set of flags for file_put_contents. One or
956
     *                          more of FILE_USE_INCLUDE_PATH, FILE_APPEND, LOCK_EX.
957
     *                          {@link http://php.net/file_put_contents}
958
     * @return  string|int<0, max>|false
959
     *
960
     * @throws InvalidArgumentException|\Random\RandomException
961
     */
962 5
    public static function fileWrite(string $file, string $data = '', int $flags = 0): string | false | int
963
    {
964
        // Sanity checks
965 5
        if (!is_readable($file)) {
966 1
            throw new InvalidArgumentException(sprintf("File '%s' does not exist or is not readable.", $file));
967
        }
968
969 5
        if (!static::isReallyWritable($file)) {
970
            throw new InvalidArgumentException(sprintf("File '%s' is not writable.", $file));
971
        }
972
973 5
        if ($flags < 0) {
974 1
            $flags = 0;
975
        }
976 5
        return file_put_contents($file, $data, $flags);
977
    }
978
979
    /** miscellaneous functions **/
980
981
    /**
982
     * Convert Fahrenheit (Fº) To Celsius (Cº)
983
     *
984
     * @since  1.2.0
985
     *
986
     * @param  float  $fahrenheit  Value in Fahrenheit
987
     * @param  bool   $rounded     Whether or not to round the result.
988
     * @param  int    $precision   Precision to use if $rounded is true.
989
     * @return float
990
     */
991 1
    public static function fahrenheitToCelsius(float $fahrenheit, bool $rounded = true, int $precision = 2): float
992
    {
993 1
        $result = ($fahrenheit - 32) / 1.8;
994
995 1
        return ($rounded) ? round($result, $precision) : $result;
996
    }
997
998
    /**
999
     * Convert Celsius (Cº) To Fahrenheit (Fº)
1000
     *
1001
     * @since  1.2.0
1002
     *
1003
     * @param  float  $celsius    Value in Celsius
1004
     * @param  bool   $rounded    Whether or not to round the result.
1005
     * @param  int    $precision  Precision to use if $rounded is true.
1006
     * @return float
1007
     */
1008 1
    public static function celsiusToFahrenheit(float $celsius, bool $rounded = true, int $precision = 2): float
1009
    {
1010 1
        $result = ($celsius * 1.8) + 32;
1011
1012 1
        return ($rounded) ? round($result, $precision) : $result;
1013
    }
1014
1015
    /**
1016
     * Convert Celsius (Cº) To Kelvin (K)
1017
     *
1018
     * @since  1.2.0
1019
     *
1020
     * @param  float  $celsius    Value in Celsius
1021
     * @param  bool   $rounded    Whether or not to round the result.
1022
     * @param  int    $precision  Precision to use if $rounded is true.
1023
     * @return float
1024
     */
1025 1
    public static function celsiusToKelvin(float $celsius, bool $rounded = true, int $precision = 2): float
1026
    {
1027 1
        $result = $celsius + 273.15;
1028
1029 1
        return ($rounded) ? round($result, $precision) : $result;
1030
    }
1031
1032
    /**
1033
     * Convert Kelvin (K) To Celsius (Cº)
1034
     *
1035
     * @since  1.2.0
1036
     *
1037
     * @param  float  $kelvin     Value in Kelvin
1038
     * @param  bool   $rounded    Whether or not to round the result.
1039
     * @param  int    $precision  Precision to use if $rounded is true.
1040
     * @return float
1041
     */
1042 1
    public static function kelvinToCelsius(float $kelvin, bool $rounded = true, int $precision = 2): float
1043
    {
1044 1
        $result = $kelvin - 273.15;
1045
1046 1
        return ($rounded) ? round($result, $precision) : $result;
1047
    }
1048
1049
    /**
1050
     * Convert Fahrenheit (Fº) To Kelvin (K)
1051
     *
1052
     * @since  1.2.0
1053
     *
1054
     * @param  float  $fahrenheit  Value in Fahrenheit
1055
     * @param  bool   $rounded     Whether or not to round the result.
1056
     * @param  int    $precision   Precision to use if $rounded is true.
1057
     * @return float
1058
     */
1059 1
    public static function fahrenheitToKelvin(float $fahrenheit, bool $rounded = true, int $precision = 2): float
1060
    {
1061 1
        $result = (($fahrenheit - 32) / 1.8) + 273.15;
1062
1063 1
        return ($rounded) ? round($result, $precision) : $result;
1064
    }
1065
1066
    /**
1067
     * Convert Kelvin (K) To Fahrenheit (Fº)
1068
     *
1069
     * @since  1.2.0
1070
     *
1071
     * @param  float  $kelvin     Value in Kelvin
1072
     * @param  bool   $rounded    Whether or not to round the result.
1073
     * @param  int    $precision  Precision to use if $rounded is true.
1074
     * @return float
1075
     */
1076 1
    public static function kelvinToFahrenheit(float $kelvin, bool $rounded = true, int $precision = 2): float
1077
    {
1078 1
        $result = (($kelvin - 273.15) * 1.8) + 32;
1079
1080 1
        return ($rounded) ? round($result, $precision) : $result;
1081
    }
1082
1083
    /**
1084
     * Convert Fahrenheit (Fº) To Rankine (ºR)
1085
     *
1086
     * @since  1.2.0
1087
     *
1088
     * @param  float  $fahrenheit  Value in Fahrenheit
1089
     * @param  bool   $rounded     Whether or not to round the result.
1090
     * @param  int    $precision   Precision to use if $rounded is true.
1091
     * @return float
1092
     */
1093 1
    public static function fahrenheitToRankine(float $fahrenheit, bool $rounded = true, int $precision = 2): float
1094
    {
1095 1
        $result = $fahrenheit + 459.67;
1096
1097 1
        return ($rounded) ? round($result, $precision) : $result;
1098
    }
1099
1100
    /**
1101
     * Convert Rankine (ºR) To Fahrenheit (Fº)
1102
     *
1103
     * @since  1.2.0
1104
     *
1105
     * @param  float  $rankine    Value in Rankine
1106
     * @param  bool   $rounded    Whether or not to round the result.
1107
     * @param  int    $precision  Precision to use if $rounded is true.
1108
     * @return float
1109
     */
1110 1
    public static function rankineToFahrenheit(float $rankine, bool $rounded = true, int $precision = 2): float
1111
    {
1112 1
        $result = $rankine - 459.67;
1113
1114 1
        return ($rounded) ? round($result, $precision) : $result;
1115
    }
1116
1117
    /**
1118
     * Convert Celsius (Cº) To Rankine (ºR)
1119
     *
1120
     * @since  1.2.0
1121
     *
1122
     * @param  float  $celsius    Value in Celsius
1123
     * @param  bool   $rounded    Whether or not to round the result.
1124
     * @param  int    $precision  Precision to use if $rounded is true.
1125
     * @return float
1126
     */
1127 1
    public static function celsiusToRankine(float $celsius, bool $rounded = true, int $precision = 2): float
1128
    {
1129 1
        $result = ($celsius * 1.8) + 491.67;
1130
1131 1
        return ($rounded) ? round($result, $precision) : $result;
1132
    }
1133
1134
    /**
1135
     * Convert Rankine (ºR) To Celsius (Cº)
1136
     *
1137
     * @since  1.2.0
1138
     *
1139
     * @param  float  $rankine    Value in Rankine
1140
     * @param  bool   $rounded    Whether or not to round the result.
1141
     * @param  int    $precision  Precision to use if $rounded is true.
1142
     * @return float
1143
     */
1144 1
    public static function rankineToCelsius(float $rankine, bool $rounded = true, int $precision = 2): float
1145
    {
1146 1
        $result = ($rankine - 491.67) / 1.8;
1147
1148 1
        return ($rounded) ? round($result, $precision) : $result;
1149
    }
1150
1151
    /**
1152
     * Convert Kelvin (K) To Rankine (ºR)
1153
     *
1154
     * @since  1.2.0
1155
     *
1156
     * @param  float  $kelvin     Value in Kelvin
1157
     * @param  bool   $rounded    Whether or not to round the result.
1158
     * @param  int    $precision  Precision to use if $rounded is true.
1159
     * @return float
1160
     */
1161 1
    public static function kelvinToRankine(float $kelvin, bool $rounded = true, int $precision = 2): float
1162
    {
1163 1
        $result = (($kelvin - 273.15) * 1.8) + 491.67;
1164
1165 1
        return ($rounded) ? round($result, $precision) : $result;
1166
    }
1167
1168
    /**
1169
     * Convert Rankine (ºR) To Kelvin (K)
1170
     *
1171
     * @since  1.2.0
1172
     *
1173
     * @param  float  $rankine    Value in Rankine
1174
     * @param  bool   $rounded    Whether or not to round the result.
1175
     * @param  int    $precision  Precision to use if $rounded is true.
1176
     * @return float
1177
     */
1178 1
    public static function rankineToKelvin(float $rankine, bool $rounded = true, int $precision = 2): float
1179
    {
1180 1
        $result = (($rankine - 491.67) / 1.8) + 273.15;
1181
1182 1
        return ($rounded) ? round($result, $precision) : $result;
1183
    }
1184
1185
    /**
1186
     * validEmail()
1187
     *
1188
     * Validate an email address using PHP's built-in filter.
1189
     *
1190
     * @param   string  $email Value to check.
1191
     * @return  bool
1192
     */
1193 2
    public static function validEmail(string $email): bool
1194
    {
1195 2
        return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
1196
    }
1197
1198
    /**
1199
     * validJson()
1200
     *
1201
     * Determines if a string is valid JSON.
1202
     *
1203
     * @param   string  $data   The string to validate as JSON.
1204
     * @return  bool
1205
     */
1206 1
    public static function validJson(string $data): bool
1207
    {
1208 1
        $data = trim($data);
1209
1210 1
        json_decode($data);
1211
1212 1
        return json_last_error() === JSON_ERROR_NONE;
1213
    }
1214
1215
    /**
1216
     * sizeFormat()
1217
     *
1218
     * Format bytes to a human-readable format.
1219
     *
1220
     * @param   int     $bytes     The number in bytes.
1221
     * @param   int     $decimals  How many decimal points to include.
1222
     * @return  string
1223
     */
1224 1
    public static function sizeFormat(int $bytes, int $decimals = 0): string
1225
    {
1226 1
        static $pow;
1227
1228
        //
1229 1
        if (is_null($pow)) {
1230 1
            $pow = [
1231 1
                'kilo'  => 1024,
1232 1
                'mega'  => 1024 ** 2,
1233 1
                'giga'  => 1024 ** 3,
1234 1
                'tera'  => 1024 ** 4,
1235 1
                'peta'  => 1024 ** 5,
1236 1
                'exa'   => 1024 ** 6,
1237 1
                'zeta'  => 1024 ** 7,
1238 1
                'yotta' => 1024 ** 8
1239 1
            ];
1240
        }
1241
1242
        //
1243 1
        $bytes = floatval($bytes);
1244
1245 1
        return match (true) {
1246 1
            $bytes >= $pow['yotta'] => number_format(($bytes / $pow['yotta']), $decimals, '.', '') . ' YiB',
1247 1
            $bytes >= $pow['zeta']  => number_format(($bytes / $pow['zeta']), $decimals, '.', '') . ' ZiB',
1248 1
            $bytes >= $pow['exa']   => number_format(($bytes / $pow['exa']), $decimals, '.', '') . ' EiB',
1249 1
            $bytes >= $pow['peta']  => number_format(($bytes / $pow['peta']), $decimals, '.', '') . ' PiB',
1250 1
            $bytes >= $pow['tera']  => number_format(($bytes / $pow['tera']), $decimals, '.', '') . ' TiB',
1251 1
            $bytes >= $pow['giga']  => number_format(($bytes / $pow['giga']), $decimals, '.', '') . ' GiB',
1252 1
            $bytes >= $pow['mega']  => number_format(($bytes / $pow['mega']), $decimals, '.', '') . ' MiB',
1253 1
            $bytes >= $pow['kilo']  => number_format(($bytes / $pow['kilo']), $decimals, '.', '') . ' KiB',
1254 1
            default => number_format($bytes, $decimals, '.', '') . ' B'
1255 1
        };
1256
    }
1257
1258
    /**
1259
     * timeDifference()
1260
     *
1261
     * Formats the difference between two timestamps to be human-readable.
1262
     *
1263
     * @param   int     $timestampFrom  Starting unix timestamp.
1264
     * @param   int     $timestampTo    Ending unix timestamp.
1265
     * @param   string  $timezone       The timezone to use. Must be a valid timezone:
1266
     *                                  {@see http://www.php.net/manual/en/timezones.php}
1267
     * @param   string  $append         The string to append to the difference.
1268
     * @return  string
1269
     *
1270
     * @throws  InvalidArgumentException|Exception
1271
     */
1272 1
    public static function timeDifference(int $timestampFrom, int $timestampTo = 0, string $timezone = 'UTC', string $append = ' old'): string
1273
    {
1274 1
        static $validTimezones;
1275
1276 1
        if (!$validTimezones) {
1277 1
            $validTimezones = DateTimeZone::listIdentifiers();
1278
        }
1279
1280
        // Check to see if it is a valid timezone
1281 1
        $timezone = ($timezone === '' ? 'UTC' : $timezone);
1282
1283 1
        if (!in_array($timezone, $validTimezones, true)) {
1284 1
            throw new InvalidArgumentException('$timezone appears to be invalid.');
1285
        }
1286
1287
        // Cannot be zero
1288 1
        if ($timestampTo <= 0) {
1289 1
            $timestampTo = time();
1290
        }
1291
1292 1
        if ($timestampFrom <= 0) {
1293
            throw new InvalidArgumentException('$timestampFrom must be greater than 0.');
1294
        }
1295
1296
        // Create \DateTime objects and set timezone
1297 1
        $timestampFrom = new DateTime(date('Y-m-d H:i:s', $timestampFrom));
1298 1
        $timestampFrom->setTimezone(new DateTimeZone($timezone));
1299
1300 1
        $timestampTo = new DateTime(date('Y-m-d H:i:s', $timestampTo));
1301 1
        $timestampTo->setTimezone(new DateTimeZone($timezone));
1302
1303
        // Calculate difference
1304 1
        $difference = $timestampFrom->diff($timestampTo);
1305
1306 1
        $string = match (true) {
1307 1
            $difference->y > 0 => $difference->y . ' year(s)',
1308 1
            $difference->m > 0 => $difference->m . ' month(s)',
1309 1
            $difference->d > 0 => (
1310 1
            $difference->d >= 7
1311 1
                ? ceil($difference->d / 7) . ' week(s)'
1312 1
                : $difference->d . ' day(s)'
1313 1
            ),
1314 1
            $difference->h > 0 => $difference->h . ' hour(s)',
1315 1
            $difference->i > 0 => $difference->i . ' minute(s)',
1316 1
            $difference->s > 0 => $difference->s . ' second(s)',
1317 1
            default => ''
1318 1
        };
1319 1
        return $string . $append;
1320
    }
1321
1322
    /**
1323
     * getIpAddress()
1324
     *
1325
     * Return the visitor's IP address.
1326
     *
1327
     * @param   bool    $trustProxy  Whether to trust HTTP_CLIENT_IP and
1328
     *                               HTTP_X_FORWARDED_FOR.
1329
     * @return  string
1330
     */
1331 1
    public static function getIpAddress(bool $trustProxy = false): string
1332
    {
1333
        // Pretty self-explanatory. Try to get an 'accurate' IP
1334 1
        if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
1335 1
            $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
1336
        }
1337
1338 1
        if (!$trustProxy) {
1339
            /** @var string **/
1340 1
            return $_SERVER['REMOTE_ADDR'];
1341
        }
1342
1343 1
        $ip = '';
1344 1
        $ips = [];
1345
1346 1
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
1347
            /** @var string $ips **/
1348 1
            $ips = $_SERVER['HTTP_X_FORWARDED_FOR'];
1349 1
            $ips = explode(',', $ips);
1350 1
        } elseif (isset($_SERVER['HTTP_X_REAL_IP'])) {
1351
            /** @var string $ips **/
1352 1
            $ips = $_SERVER['HTTP_X_REAL_IP'];
1353 1
            $ips = explode(',', $ips);
1354
        }
1355
1356
        /** @var  array<mixed> $ips **/
1357 1
        $ips = static::arrayMapDeep($ips, 'trim');
1358
1359 1
        if (count($ips) > 0) {
1360 1
            foreach ($ips as $val) {
1361
                /** @phpstan-ignore-next-line */
1362 1
                if (inet_ntop(inet_pton($val)) === $val && static::isPublicIp($val)) {
1363
                    /** @var string $ip **/
1364 1
                    $ip = $val;
1365 1
                    break;
1366
                }
1367
            }
1368
        }
1369 1
        unset($ips);
1370
1371 1
        if ($ip === '') {
1372
            /** @var string $ip **/
1373 1
            $ip = $_SERVER['HTTP_CLIENT_IP'] ?? $_SERVER['REMOTE_ADDR'];
1374
        }
1375 1
        return $ip;
1376
    }
1377
1378
    /**
1379
     * isPrivateIp()
1380
     *
1381
     * Determines if an IP address is within the private range.
1382
     *
1383
     * @param   string  $ipaddress  IP address to check.
1384
     * @return  bool
1385
     */
1386 3
    public static function isPrivateIp(string $ipaddress): bool
1387
    {
1388 3
        return !(bool) filter_var(
1389 3
            $ipaddress,
1390 3
            FILTER_VALIDATE_IP,
1391 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE
1392 3
        );
1393
    }
1394
1395
    /**
1396
     * isReservedIp()
1397
     *
1398
     * Determines if an IP address is within the reserved range.
1399
     *
1400
     * @param   string  $ipaddress  IP address to check.
1401
     * @return  bool
1402
     */
1403 3
    public static function isReservedIp(string $ipaddress): bool
1404
    {
1405 3
        return !(bool) filter_var(
1406 3
            $ipaddress,
1407 3
            FILTER_VALIDATE_IP,
1408 3
            FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_RES_RANGE
1409 3
        );
1410
    }
1411
1412
    /**
1413
     * isPublicIp()
1414
     *
1415
     * Determines if an IP address is not within the private or reserved ranges.
1416
     *
1417
     * @param   string  $ipaddress  IP address to check.
1418
     * @return  bool
1419
     */
1420 2
    public static function isPublicIp(string $ipaddress): bool
1421
    {
1422 2
        return (!static::isPrivateIp($ipaddress) && !static::isReservedIp($ipaddress));
1423
    }
1424
1425
    /**
1426
     * obscureEmail()
1427
     *
1428
     * Obscures an email address.
1429
     *
1430
     * @param   string  $email  Email address to obscure.
1431
     * @return  string          Obscured email address.
1432
     *
1433
     * @throws  InvalidArgumentException
1434
     */
1435 1
    public static function obscureEmail(string $email): string
1436
    {
1437
        // Sanity check
1438 1
        if (!static::validEmail($email)) {
1439 1
            throw new InvalidArgumentException('Invalid $email specified.');
1440
        }
1441
1442
        // Split and process
1443 1
        $email = array_map(function($char) {
1444 1
            return '&#' . ord($char) . ';';
1445 1
        }, str_split($email));
1 ignored issue
show
Bug introduced by
It seems like str_split($email) can also be of type true; however, parameter $array of array_map() does only seem to accept 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

1445
        }, /** @scrutinizer ignore-type */ str_split($email));
Loading history...
1446
1447 1
        return implode('', $email);
1448
    }
1449
1450
    /**
1451
     * currentHost()
1452
     *
1453
     * Determines current hostname.
1454
     *
1455
     * @param   bool    $stripWww         True to strip www. off the host, false to leave it be.
1456
     * @param   bool    $acceptForwarded  True to accept 
1457
     * @return  string
1458
     */
1459 2
    public static function currentHost(bool $stripWww = false, bool $acceptForwarded = false): string
1460
    {
1461
        /** @var string $host **/
1462 2
        $host = (
1463 2
            ($acceptForwarded && isset($_SERVER['HTTP_X_FORWARDED_HOST'])) ? 
1464 1
            $_SERVER['HTTP_X_FORWARDED_HOST'] : 
1465 2
            ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '')
1466 2
        );
1467 2
        $host = trim(strval($host));
1468
1469 2
        if ($host === '' || preg_match('#^\[?(?:[a-z0-9-:\]_]+\.?)+$#', $host) === 0) {
1470 1
            $host = 'localhost';
1471
        }
1472
1473 2
        $host = static::lower($host);
1474
1475
        // Strip 'www.'
1476 2
        if ($stripWww) {
1477 1
            $strippedHost = preg_replace('#^www\.#', '', $host);
1478
        }
1479 2
        return ($strippedHost ?? $host);
1480
    }
1481
1482
    /**
1483
     * serverHttpVars()
1484
     *
1485
     * Builds an array of headers based on HTTP_* keys within $_SERVER.
1486
     *
1487
     * @param   bool  $asLowerCase
1488
     * @return  array<mixed>
1489
     */
1490 3
    public static function serverHttpVars(bool $asLowerCase = false): array
1491
    {
1492 3
        $headers = [];
1493
1494 3
        if (static::doesNotContain(PHP_SAPI, 'cli')) {
1495
            /** @var array<mixed> $keys **/
1496
            $keys = static::arrayMapDeep(array_keys($_SERVER), [static::class, 'lower']);
1497
            $keys = array_filter($keys, function($key) {
1498
                /** @var string $key **/
1499
                return (static::beginsWith($key, 'http_'));
1500
            });
1501
1502
            if (count($keys) > 0) {
1503
                foreach ($keys as $key) {
1504
                    /** @var string $key **/
1505
                    $headers[strtr(
1506
                        ucwords(strtr(static::substr($key, 5), '_', ' ')),
1507
                        ' ',
1508
                        '-'
1509
                    )] = &$_SERVER[($asLowerCase ? $key : static::upper($key))];
1510
                }
1511
            }
1512
            unset($keys);
1513
        }
1514 3
        return $headers;
1515
    }
1516
1517
    /**
1518
     * isHttps()
1519
     *
1520
     * Checks to see if SSL is in use.
1521
     *
1522
     * @return  bool
1523
     */
1524 2
    public static function isHttps(): bool
1525
    {
1526 2
        $headers = static::serverHttpVars(true);
1527
1528
        // Generally, as long as HTTPS is not set or is any empty value, it is considered to be "off"
1529
        if (
1530 2
            (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off')
1531 2
            || (isset($headers['X-Forwarded-Proto']) && $headers['X-Forwarded-Proto'] === 'https')
1532 2
            || (isset($headers['Front-End-Https']) && $headers['Front-End-Https'] !== 'off')
1533
        ) {
1534 2
            return true;
1535
        }
1536 2
        return false;
1537
    }
1538
1539
1540
    /**
1541
     * currentUrl()
1542
     *
1543
     * Retrieve the current URL.
1544
     *
1545
     * @param   bool   $parse  True to return the url as an array, false otherwise.
1546
     * @return  mixed
1547
     */
1548 1
    public static function currentUrl(bool $parse = false): mixed
1549
    {
1550
        // Scheme
1551 1
        $scheme = (static::isHttps()) ? 'https://' : 'http://';
1552
1553
        // Auth
1554 1
        $auth = '';
1555
1556 1
        if (isset($_SERVER['PHP_AUTH_USER'])) {
1557 1
            $auth = $_SERVER['PHP_AUTH_USER'] . (
1558 1
                isset($_SERVER['PHP_AUTH_PW']) ? ':' . $_SERVER['PHP_AUTH_PW'] : ''
1559 1
            ) . '@';
1560
        }
1561
1562
        // Host and port
1563 1
        $host = static::currentHost();
1564
1565
        /** @var int $port **/
1566 1
        $port = $_SERVER['SERVER_PORT'] ?? 0;
1567 1
        $port = ($port === (static::isHttps() ? 443 : 80)) ? 0 : $port;
1568
1569
        // Path
1570
        /** @var string $self **/
1571 1
        $self = $_SERVER['PHP_SELF'];
1572
        /** @var string $query **/
1573 1
        $query = $_SERVER['QUERY_STRING'] ?? '';
1574
        /** @var string $request **/
1575 1
        $request = $_SERVER['REQUEST_URI'] ?? '';
1576
        /** @var string $path **/
1577 1
        $path = ($request === '' ? $self . ($query !== '' ? '?' . $query : '') : $request);
1578
1579
        // Put it all together
1580
        /** @var string $url **/
1581 1
        $url = sprintf('%s%s%s%s%s', $scheme, $auth, $host, ($port > 0 ? ":$port" : ''), $path);
1582
1583
        // If $parse is true, parse into array
1584 1
        return ($parse ? parse_url($url) : $url);
1585
    }
1586
1587
    /**
1588
     * ordinal()
1589
     *
1590
     * Retrieve the ordinal version of a number.
1591
     *
1592
     * Basically, it will append th, st, nd, or rd based on what the number ends with.
1593
     *
1594
     * @param   int     $number  The number to create an ordinal version of.
1595
     * @return  string
1596
     */
1597 1
    public static function ordinal(int $number): string
1598
    {
1599 1
        static $suffixes = ['th', 'st', 'nd', 'rd'];
1600
1601 1
        if (abs($number) % 100 > 10 && abs($number) % 100 < 20) {
1602 1
            $suffix = $suffixes[0];
1603 1
        } elseif (abs($number) % 10 < 4) {
1604 1
            $suffix = $suffixes[(abs($number) % 10)];
1605
        } else {
1606 1
            $suffix = $suffixes[0];
1607
        }
1608 1
        return $number . $suffix;
1609
    }
1610
1611
    /**
1612
     * statusHeader()
1613
     *
1614
     * Send an HTTP status header.
1615
     *
1616
     * @param  int     $code     The status code.
1617
     * @param  string  $message  Custom status message.
1618
     * @param  bool    $replace  True if the header should replace a previous similar header.
1619
     *                           False to add a second header of the same type
1620
     *
1621
     * @throws Exception|InvalidArgumentException|RuntimeException
1622
     *
1623
     * @deprecated 1.2.0         Use \http_response_code() instead
1624
     */
1625 1
    public static function statusHeader(int $code = 200, string $message = '', bool $replace = true): void
1626
    {
1627 1
        static $statusCodes;
1628
1629 1
        @trigger_error('Utility function "statusHeader()" is deprecated since version 1.2.0. Use \http_response_code() instead', \E_USER_DEPRECATED);
1630
1631 1
        if (!$statusCodes) {
1632 1
            $statusCodes = [
1633 1
                100    => 'Continue',
1634 1
                101    => 'Switching Protocols',
1635 1
                200    => 'OK',
1636 1
                201    => 'Created',
1637 1
                202    => 'Accepted',
1638 1
                203    => 'Non-Authoritative Information',
1639 1
                204    => 'No Content',
1640 1
                205    => 'Reset Content',
1641 1
                206    => 'Partial Content',
1642 1
                300    => 'Multiple Choices',
1643 1
                301    => 'Moved Permanently',
1644 1
                302    => 'Found',
1645 1
                303    => 'See Other',
1646 1
                304    => 'Not Modified',
1647 1
                305    => 'Use Proxy',
1648 1
                307    => 'Temporary Redirect',
1649 1
                400    => 'Bad Request',
1650 1
                401    => 'Unauthorized',
1651 1
                402    => 'Payment Required',
1652 1
                403    => 'Forbidden',
1653 1
                404    => 'Not Found',
1654 1
                405    => 'Method Not Allowed',
1655 1
                406    => 'Not Acceptable',
1656 1
                407    => 'Proxy Authentication Required',
1657 1
                408    => 'Request Timeout',
1658 1
                409    => 'Conflict',
1659 1
                410    => 'Gone',
1660 1
                411    => 'Length Required',
1661 1
                412    => 'Precondition Failed',
1662 1
                413    => 'Request Entity Too Large',
1663 1
                414    => 'Request-URI Too Long',
1664 1
                415    => 'Unsupported Media Type',
1665 1
                416    => 'Requested Range Not Satisfiable',
1666 1
                417    => 'Expectation Failed',
1667 1
                422    => 'Unprocessable Entity',
1668 1
                500    => 'Internal Server Error',
1669 1
                501    => 'Not Implemented',
1670 1
                502    => 'Bad Gateway',
1671 1
                503    => 'Service Unavailable',
1672 1
                504    => 'Gateway Timeout',
1673 1
                505    => 'HTTP Version Not Supported'
1674 1
            ];
1675
        }
1676
1677
        // Sanity check
1678 1
        if ($code < 0 || $code > 505) {
1679 1
            throw new InvalidArgumentException('$code is invalid.');
1680
        }
1681
1682 1
        if ($message === '') {
1683 1
            if (!isset($statusCodes[$code])) {
1684
                throw new Exception('No status message available. Please double check your $code or provide a custom $message.');
1685
            }
1686 1
            $message = $statusCodes[$code];
1687
        }
1688
1689 1
        if (headers_sent($line, $file)) {
1690
            throw new RuntimeException(sprintf('Failed to send header. Headers have already been sent by "%s" at line %d.', $file, $line));
1691
        }
1692
1693
        // Properly format and send header, based on server API
1694 1
        if (static::doesContain(PHP_SAPI, 'cgi')) {
1695
            header("Status: $code $message", $replace);
1696
        } else {
1697 1
            header(
1698 1
                ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1') . " $code $message",
1699 1
                $replace,
1700 1
                $code
1701 1
            );
1702
        }
1703
    }
1704
1705
    /**
1706
     * guid()
1707
     *
1708
     * Generate a Globally/Universally Unique Identifier (version 4).
1709
     *
1710
     * @return  string
1711
     * @throws \Random\RandomException
1712
     */
1713 1
    public static function guid(): string
1714
    {
1715 1
        static $format = '%04x%04x-%04x-%04x-%04x-%04x%04x%04x';
1716
1717
        try {
1718 1
            $guid = sprintf(
1719 1
                $format,
1720 1
                static::randomInt(0, 0xffff),
1721 1
                static::randomInt(0, 0xffff),
1722 1
                static::randomInt(0, 0xffff),
1723 1
                static::randomInt(0, 0x0fff) | 0x4000,
1724 1
                static::randomInt(0, 0x3fff) | 0x8000,
1725 1
                static::randomInt(0, 0xffff),
1726 1
                static::randomInt(0, 0xffff),
1727 1
                static::randomInt(0, 0xffff)
1728 1
            );
1729
        } catch (\Random\RandomException $e) {
1730
            throw new \Random\RandomException('Unable to generate GUID: ' . $e->getMessage(), 0, $e);
1731
        }
1732 1
        return $guid;
1733
    }
1734
1735
    /**
1736
     * timezoneInfo()
1737
     *
1738
     * Retrieves information about a timezone.
1739
     *
1740
     * Note: Must be a valid timezone recognized by PHP.
1741
     *
1742
     * @see http://www.php.net/manual/en/timezones.php
1743
     *
1744
     * @param   string  $timezone  The timezone to return information for.
1745
     * @return  array<mixed>
1746
     *
1747
     * @throws  InvalidArgumentException|Exception
1748
     */
1749 1
    public static function timezoneInfo(string $timezone): array
1750
    {
1751 1
        static $validTimezones;
1752
1753 1
        if (!$validTimezones) {
1754 1
            $validTimezones = DateTimeZone::listIdentifiers();
1755
        }
1756
1757
        // Check to see if it is a valid timezone
1758 1
        $timezone = ($timezone === '' ? 'UTC' : $timezone);
1759
1760 1
        if (!in_array($timezone, $validTimezones, true)) {
1761
            throw new InvalidArgumentException('$timezone appears to be invalid.');
1762
        }
1763
1764
        try {
1765 1
            $tz = new DateTimeZone($timezone);
1766
        } catch (Exception $e) {
1767
            throw new InvalidArgumentException($e->getMessage(), 0, $e);
1768
        }
1769
1770 1
        $location = $tz->getLocation();
1771
1772 1
        if ($location === false) {
1773
            $location = [
1774
                'country_code' => 'N/A',
1775
                'latitude'     => 'N/A',
1776
                'longitude'    => 'N/A'
1777
            ];
1778
        }
1779
1780 1
        $info = [
1781 1
            'offset'    => $tz->getOffset(new DateTime('now', new DateTimeZone('GMT'))) / 3600,
1782 1
            'country'   => $location['country_code'],
1783 1
            'latitude'  => $location['latitude'],
1784 1
            'longitude' => $location['longitude'],
1785 1
            'dst'       => $tz->getTransitions($now = time(), $now)[0]['isdst']
1786 1
        ];
1787 1
        unset($tz);
1788
1789 1
        return $info;
1790
    }
1791
1792
    /**
1793
     * iniGet()
1794
     *
1795
     * Safe ini_get taking into account its availability.
1796
     *
1797
     * @param   string  $option       The configuration option name.
1798
     * @param   bool    $standardize  Standardize returned values to 1 or 0?
1799
     * @return  string|false
1800
     *
1801
     * @throws  RuntimeException|InvalidArgumentException
1802
     */
1803 3
    public static function iniGet(string $option, bool $standardize = false): string | false
1804
    {
1805 3
        if (!function_exists('\\ini_get')) {
1806
            // disabled_functions?
1807
            throw new RuntimeException('Native ini_get function not available.');
1808
        }
1809
1810 3
        if ($option === '') {
1811 1
            throw new InvalidArgumentException('$option must not be empty.');
1812
        }
1813
1814 3
        $value = ini_get($option);
1815
1816 3
        if ($value === false) {
1817
            throw new RuntimeException('$option does not exist.');
1818
        }
1819
1820 3
        $value = trim($value);
1821
1822 3
        if ($standardize) {
1823 1
            $value = match (static::lower($option)) {
1824 1
                'yes', 'on', 'true', '1' => '1',
1825 1
                'no', 'off', 'false', '0' => '0',
1826 1
                default => $value
1827 1
            };
1828
        }
1829 3
        return $value;
1830
    }
1831
1832
    /**
1833
     * iniSet()
1834
     *
1835
     * Safe ini_set taking into account its availability.
1836
     *
1837
     * @param   string  $option  The configuration option name.
1838
     * @param   string  $value   The new value for the option.
1839
     * @return  string|false
1840
     *
1841
     * @throws RuntimeException|InvalidArgumentException
1842
     */
1843 2
    public static function iniSet(string $option, string $value): string | false
1844
    {
1845 2
        if (!function_exists('\\ini_set')) {
1846
            // disabled_functions?
1847
            throw new RuntimeException('Native ini_set function not available.');
1848
        }
1849
1850 2
        if ($option === '') {
1851
            throw new InvalidArgumentException('$option must not be empty.');
1852
        }
1853 2
        return ini_set($option, $value);
1854
    }
1855
}
1856