Passed
Push — master ( 5b74be...84d61d )
by Eric
18:51 queued 06:03
created

Strings   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 442
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 78
dl 0
loc 442
c 0
b 0
f 0
ccs 105
cts 105
cp 1
rs 9.36
wmc 38
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Utility - Collection of various PHP utility functions.
7
 *
8
 * @author    Eric Sizemore <[email protected]>
9
 * @version   2.0.0
10
 * @copyright (C) 2017 - 2024 Eric Sizemore
11
 * @license   The MIT License (MIT)
12
 *
13
 * Copyright (C) 2017 - 2024 Eric Sizemore <https://www.secondversion.com>.
14
 *
15
 * Permission is hereby granted, free of charge, to any person obtaining a copy
16
 * of this software and associated documentation files (the "Software"), to
17
 * deal in the Software without restriction, including without limitation the
18
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
19
 * sell copies of the Software, and to permit persons to whom the Software is
20
 * furnished to do so, subject to the following conditions:
21
 *
22
 * The above copyright notice and this permission notice shall be included in
23
 * all copies or substantial portions of the Software.
24
 *
25
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31
 * THE SOFTWARE.
32
 */
33
34
namespace Esi\Utility;
35
36
// Exceptions
37
use InvalidArgumentException;
38
use Random\RandomException;
39
use ValueError;
40
41
// Functions
42
use voku\helper\ASCII;
43
44
use function mb_convert_case;
45
use function mb_substr;
46
use function strcmp;
47
use function mb_strpos;
48
use function str_starts_with;
49
use function str_ends_with;
50
use function str_contains;
51
use function mb_strlen;
52
use function str_replace;
53
use function preg_replace;
54
use function preg_quote;
55
use function trim;
56
use function random_bytes;
57
use function bin2hex;
58
use function filter_var;
59
use function json_decode;
60
use function json_last_error;
61
use function array_map;
62
use function ord;
63
use function implode;
64
use function str_split;
65
use function sprintf;
66
use function preg_replace_callback;
67
use function ltrim;
68
69
// Constants
70
use const MB_CASE_TITLE;
71
use const MB_CASE_LOWER;
72
use const MB_CASE_UPPER;
73
use const FILTER_VALIDATE_EMAIL;
74
use const JSON_ERROR_NONE;
75
76
/**
77
 * String utilities.
78
 */
79
final class Strings
80
{
81
    /**
82
     * Encoding to use for multibyte-based functions.
83
     *
84
     * @var string $encoding Encoding
85
     */
86
    private static string $encoding = 'UTF-8';
87
88
    /**
89
     * Returns current encoding.
90
     *
91
     * @return string Current encoding.
92
     */
93 2
    public static function getEncoding(): string
94
    {
95 2
        return self::$encoding;
96
    }
97
98
    /**
99
     * Sets the encoding to use for multibyte-based functions.
100
     *
101
     * @param  string  $newEncoding  Charset.
102
     * @param  bool    $iniUpdate    Update php.ini's default_charset?
103
     */
104 4
    public static function setEncoding(string $newEncoding = '', bool $iniUpdate = false): void
105
    {
106 4
        if ($newEncoding !== '') {
107 4
            self::$encoding = $newEncoding;
108
        }
109
110 4
        if ($iniUpdate) {
111 1
            Environment::iniSet('default_charset', self::$encoding);
112 1
            Environment::iniSet('internal_encoding', self::$encoding);
113
        }
114
    }
115
116
    /**
117
     * title()
118
     *
119
     * Convert the given string to title case.
120
     *
121
     * @param   string  $value  Value to convert.
122
     * @return  string          Value converted to titlecase.
123
     */
124 1
    public static function title(string $value): string
125
    {
126 1
        return mb_convert_case($value, MB_CASE_TITLE, (self::$encoding ?? null));
127
    }
128
129
    /**
130
     * lower()
131
     *
132
     * Convert the given string to lower case.
133
     *
134
     * @param   string  $value  Value to convert.
135
     * @return  string          Value converted to lowercase.
136
     */
137 26
    public static function lower(string $value): string
138
    {
139 26
        return mb_convert_case($value, MB_CASE_LOWER, (self::$encoding ?? null));
140
    }
141
142
    /**
143
     * upper()
144
     *
145
     * Convert the given string to upper case.
146
     *
147
     * @param   string  $value  Value to convert.
148
     * @return  string          Value converted to uppercase.
149
     */
150 20
    public static function upper(string $value): string
151
    {
152 20
        return mb_convert_case($value, MB_CASE_UPPER, (self::$encoding ?? null));
153
    }
154
155
    /**
156
     * substr()
157
     *
158
     * Returns the portion of string specified by the start and length parameters.
159
     *
160
     * @param   string    $string  The input string.
161
     * @param   int       $start   Start position.
162
     * @param   int|null  $length  Characters from $start.
163
     * @return  string             Extracted part of the string.
164
     */
165 24
    public static function substr(string $string, int $start, ?int $length = null): string
166
    {
167 24
        return mb_substr($string, $start, $length, (self::$encoding ?? null));
168
    }
169
170
    /**
171
     * lcfirst()
172
     *
173
     * Convert the first character of a given string to lower case.
174
     *
175
     * @since   1.0.1
176
     *
177
     * @param   string  $string  The input string.
178
     * @return  string           String with the first letter lowercase'd.
179
     */
180 20
    public static function lcfirst(string $string): string
181
    {
182 20
        return self::lower(self::substr($string, 0, 1)) . self::substr($string, 1);
183
    }
184
185
    /**
186
     * ucfirst()
187
     *
188
     * Convert the first character of a given string to upper case.
189
     *
190
     * @since   1.0.1
191
     *
192
     * @param   string  $string  The input string.
193
     * @return  string           String with the first letter uppercase'd.
194
     */
195 1
    public static function ucfirst(string $string): string
196
    {
197 1
        return self::upper(self::substr($string, 0, 1)) . self::substr($string, 1);
198
    }
199
200
    /**
201
     * Compares multibyte input strings in a binary safe case-insensitive manner.
202
     *
203
     * @since  1.0.1
204
     *
205
     * @param  string  $str1  The first string.
206
     * @param  string  $str2  The second string.
207
     * @return int            Returns < 0 if $str1 is less than $str2; > 0 if $str1
208
     *                        is greater than $str2, and 0 if they are equal.
209
     */
210 1
    public static function strcasecmp(string $str1, string $str2): int
211
    {
212 1
        return strcmp(Strings::upper($str1), Strings::upper($str2));
213
    }
214
215
    /**
216
     * beginsWith()
217
     *
218
     * Determine if a string begins with another string.
219
     *
220
     * @param   string  $haystack     String to search in.
221
     * @param   string  $needle       String to check for.
222
     * @param   bool    $insensitive  True to do a case-insensitive search.
223
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
224
     * @return  bool                  True if the string begins with $needle, false otherwise.
225
     */
226 1
    public static function beginsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
227
    {
228 1
        if ($insensitive) {
229 1
            $haystack = Strings::lower($haystack);
230 1
            $needle   = Strings::lower($needle);
231
        }
232 1
        return (
233 1
            $multibyte
234 1
            ? mb_strpos($haystack, $needle) === 0
235 1
            : str_starts_with($haystack, $needle)
236 1
        );
237
    }
238
239
    /**
240
     * endsWith()
241
     *
242
     * Determine if a string ends with another string.
243
     *
244
     * @param   string  $haystack     String to search in.
245
     * @param   string  $needle       String to check for.
246
     * @param   bool    $insensitive  True to do a case-insensitive search.
247
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
248
     * @return  bool                  True if the string ends with $needle, false otherwise.
249
     */
250 1
    public static function endsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
251
    {
252 1
        if ($insensitive) {
253 1
            $haystack = Strings::lower($haystack);
254 1
            $needle   = Strings::lower($needle);
255
        }
256 1
        return (
257 1
            $multibyte
258 1
            ? Strings::substr($haystack, -Strings::length($needle)) === $needle
259 1
            : str_ends_with($haystack, $needle)
260 1
        );
261
    }
262
263
    /**
264
     * doesContain()
265
     *
266
     * Determine if a string exists within another string.
267
     *
268
     * @param   string  $haystack     String to search in.
269
     * @param   string  $needle       String to check for.
270
     * @param   bool    $insensitive  True to do a case-insensitive search.
271
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
272
     * @return  bool                  True if the string contains $needle, false otherwise.
273
     */
274 2
    public static function doesContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
275
    {
276 2
        if ($insensitive) {
277 2
            $haystack = Strings::lower($haystack);
278 2
            $needle   = Strings::lower($needle);
279
        }
280 2
        return (
281 2
            $multibyte
282 2
            ? mb_strpos($haystack, $needle) !== false
283 2
            : str_contains($haystack, $needle)
284 2
        );
285
    }
286
287
    /**
288
     * doesNotContain()
289
     *
290
     * Determine if a string does not exist within another string.
291
     *
292
     * @param   string  $haystack     String to search in.
293
     * @param   string  $needle       String to check for.
294
     * @param   bool    $insensitive  True to do a case-insensitive search.
295
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
296
     * @return  bool                  True if the string does not contain $needle, false otherwise.
297
     */
298 1
    public static function doesNotContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
299
    {
300 1
        return (!Strings::doesContain($haystack, $needle, $insensitive, $multibyte));
301
    }
302
303
    /**
304
     * length()
305
     *
306
     * Get string length.
307
     *
308
     * @param   string  $string      The string being checked for length.
309
     * @param   bool    $binarySafe  Forces '8bit' encoding so that the length check is binary safe.
310
     * @return  int                  The length of the given string.
311
     */
312 2
    public static function length(string $string, bool $binarySafe = false): int
313
    {
314 2
        return mb_strlen($string, ($binarySafe ? '8bit' : self::$encoding));
315
    }
316
317
    /**
318
     * camelCase()
319
     *
320
     * Returns a camelCase version of the string.
321
     *
322
     * Shoutout to Daniel St. Jules (https://github.com/danielstjules/Stringy/) for
323
     * inspiration for this function. This function is based on Stringy/camelize().
324
     *
325
     * My changes were mainly to make it fit into Utility.
326
     *
327
     * @since 2.0.0
328
     *
329
     * @param  string  $string  String to camelCase.
330
     * @return string           camelCase'd string.
331
     */
332 19
    public static function camelCase(string $string): string
333
    {
334
        // Trim surrounding spaces
335 19
        $string = trim($string);
336 19
        $string = self::lcfirst($string);
337
338
        // Remove leading dashes and underscores
339 19
        $string = ltrim($string, '-_');
340
341
        // Transformation
342 19
        $transformation = (string) preg_replace_callback(
343 19
            '/[-_\s]+(.)?/u',
344 19
            static fn (array $match): string => isset($match[1]) ? self::upper($match[1]) : '',
345 19
            $string
346 19
        );
347 19
        return (string) preg_replace_callback('/\p{N}+(.)?/u', static fn (array $match): string => self::upper($match[0]), $transformation);
348
    }
349
350
    /**
351
     * ascii()
352
     *
353
     * Transliterate a UTF-8 value to ASCII.
354
     *
355
     * @param   string  $value     Value to transliterate.
356
     * @param   string  $language  Language code (2 characters, eg: en). {@see ASCII}
357
     * @return  string             Value as ASCII.
358
     */
359 3
    public static function ascii(string $value, string $language = 'en'): string
360
    {
361
        /** @var ASCII::*_LANGUAGE_CODE $language */
362 3
        return ASCII::to_ascii($value, $language);
363
    }
364
365
    /**
366
     * slugify()
367
     *
368
     * Transforms a string into a URL or filesystem-friendly string.
369
     *
370
     * Note: Adapted from Illuminate/Support/Str::slug.
371
     *
372
     * @see https://packagist.org/packages/illuminate/support#v6.20.44
373
     * * @see http://opensource.org/licenses/MIT
374
     *
375
     * @param   string   $title      String to convert.
376
     * @param   string   $separator  Separator used to separate words in $title.
377
     * @param   ?string  $language   Language code (2 characters, eg: en). {@see ASCII}
378
     * @return  string               Transformed string.
379
     */
380 1
    public static function slugify(string $title, string $separator = '-', ?string $language = 'en'): string
381
    {
382 1
        $title = $language !== null ? Strings::ascii($title) : $title;
383
384
        // Replace @ with the word 'at'
385 1
        $title = str_replace('@', $separator . 'at' . $separator, $title);
386
387 1
        $title = (string) preg_replace('![' . preg_quote(($separator === '-' ? '_' : '-')) . ']+!u', $separator, $title);
388
389
        // Remove all characters that are not the separator, letters, numbers, or whitespace.
390 1
        $title = (string) preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', Strings::lower($title));
391
392
        // Replace all separator characters and whitespace by a single separator
393 1
        $title = (string) preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title);
394
395
        // Cleanup $title
396 1
        return trim($title, $separator);
397
    }
398
399
    /**
400
     * randomBytes()
401
     *
402
     * Generate cryptographically secure pseudo-random bytes.
403
     *
404
     * @param   int<1, max>  $length  Length of the random string that should be returned in bytes.
405
     * @return  string                Random bytes of $length length.
406
     *
407
     * @throws RandomException
408
     * @throws ValueError
409
     */
410 2
    public static function randomBytes(int $length): string
411
    {
412
        // Sanity check
413
        // @phpstan-ignore-next-line
414 2
        if ($length < 1) {
415 1
            throw new RandomException('$length must be greater than 1.');
416
        }
417
418
        // Generate bytes
419 2
        return random_bytes($length);
420
    }
421
422
    /**
423
     * randomString()
424
     *
425
     * Generates a secure random string, based on {@see static::randomBytes()}.
426
     *
427
     * @param   int<min, max>  $length  Length the random string should be.
428
     * @return  string                  Random string of $length length.
429
     *
430
     * @throws RandomException | ValueError
431
     */
432 1
    public static function randomString(int $length = 8): string
433
    {
434
        // Sanity check
435 1
        if ($length < 1) {
436 1
            throw new RandomException('$length must be greater than 1.');
437
        }
438
        // Convert bytes to hexadecimal and truncate to the desired length
439 1
        return Strings::substr(bin2hex(Strings::randomBytes($length * 2)), 0, $length);
440
    }
441
442
    /**
443
     * validEmail()
444
     *
445
     * Validate an email address using PHP's built-in filter.
446
     *
447
     * @param   string  $email Value to check.
448
     * @return  bool           True if the email is valid, false otherwise.
449
     */
450 2
    public static function validEmail(string $email): bool
451
    {
452 2
        return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
453
    }
454
455
    /**
456
     * validJson()
457
     *
458
     * Determines if a string is valid JSON.
459
     *
460
     * @deprecated as of 2.0.0
461
     *
462
     * @param   string  $data  The string to validate as JSON.
463
     * @return  bool           True if the json appears to be valid, false otherwise.
464
     */
465 1
    public static function validJson(string $data): bool
466
    {
467 1
        $data = trim($data);
468 1
        json_decode($data);
469
470 1
        return (json_last_error() === JSON_ERROR_NONE);
471
    }
472
473
    /**
474
     * obscureEmail()
475
     *
476
     * Obscures an email address.
477
     *
478
     * @param   string  $email  Email address to obscure.
479
     * @return  string          Obscured email address.
480
     *
481
     * @throws  InvalidArgumentException
482
     */
483 1
    public static function obscureEmail(string $email): string
484
    {
485
        // Sanity check
486 1
        if (!Strings::validEmail($email)) {
487 1
            throw new InvalidArgumentException('Invalid $email specified.');
488
        }
489
490
        // Split and process
491 1
        $email = array_map(
492 1
            static fn (string $char): string => '&#' . ord($char) . ';',
493
            /** @scrutinizer ignore-type */
494 1
            str_split($email)
495 1
        );
496
497 1
        return implode('', $email);
498
    }
499
500
    /**
501
     * guid()
502
     *
503
     * Generate a Globally/Universally Unique Identifier (version 4).
504
     *
505
     * @return  string  Random GUID.
506
     *
507
     * @throws RandomException
508
     */
509 1
    public static function guid(): string
510
    {
511 1
        return sprintf(
512 1
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
513 1
            Numbers::random(0, 0xff_ff),
1 ignored issue
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ')' on line 513 at column 35
Loading history...
514 1
            Numbers::random(0, 0xff_ff),
515 1
            Numbers::random(0, 0xff_ff),
516 1
            Numbers::random(0, 0x0f_ff) | 0x40_00,
517 1
            Numbers::random(0, 0x3f_ff) | 0x80_00,
518 1
            Numbers::random(0, 0xff_ff),
519 1
            Numbers::random(0, 0xff_ff),
520 1
            Numbers::random(0, 0xff_ff)
521 1
        );
522
    }
523
}
524