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

Utility::statusHeader()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 76
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 8.0076

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 8
eloc 60
c 3
b 0
f 0
nc 16
nop 3
dl 0
loc 76
ccs 58
cts 61
cp 0.9508
crap 8.0076
rs 7.6282

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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