Passed
Push — master ( eb151a...2c3ea5 )
by Eric
02:25
created

Strings::doesContain()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
39
use ValueError;
40
41
// Functions
42
use 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
85
     */
86
    private static string $encoding = 'UTF-8';
87
88
    /**
89
     * Returns current encoding.
90
     *
91
     */
92 2
    public static function getEncoding(): string
93
    {
94 2
        return self::$encoding;
95
    }
96
97
    /**
98
     * Sets the encoding to use for multibyte-based functions.
99
     *
100
     * @param  string  $newEncoding  Charset.
101
     * @param  bool    $iniUpdate    Update php.ini's default_charset?
102
     */
103 4
    public static function setEncoding(string $newEncoding = '', bool $iniUpdate = false): void
104
    {
105 4
        if ($newEncoding !== '') {
106 4
            self::$encoding = $newEncoding;
107
        }
108
109 4
        if ($iniUpdate) {
110 1
            Environment::iniSet('default_charset', self::$encoding);
111 1
            Environment::iniSet('internal_encoding', self::$encoding);
112
        }
113
    }
114
115
    /**
116
     * title()
117
     *
118
     * Convert the given string to title case.
119
     *
120
     * @param   string  $value  Value to convert.
121
     * @return  string
122
     */
123 1
    public static function title(string $value): string
124
    {
125 1
        return mb_convert_case($value, MB_CASE_TITLE, (self::$encoding ?? null));
126
    }
127
128
    /**
129
     * lower()
130
     *
131
     * Convert the given string to lower case.
132
     *
133
     * @param   string  $value  Value to convert.
134
     * @return  string
135
     */
136 26
    public static function lower(string $value): string
137
    {
138 26
        return mb_convert_case($value, MB_CASE_LOWER, (self::$encoding ?? null));
139
    }
140
141
    /**
142
     * upper()
143
     *
144
     * Convert the given string to upper case.
145
     *
146
     * @param   string  $value  Value to convert.
147
     * @return  string
148
     */
149 20
    public static function upper(string $value): string
150
    {
151 20
        return mb_convert_case($value, MB_CASE_UPPER, (self::$encoding ?? null));
152
    }
153
154
    /**
155
     * substr()
156
     *
157
     * Returns the portion of string specified by the start and length parameters.
158
     *
159
     * @param   string    $string  The input string.
160
     * @param   int       $start   Start position.
161
     * @param   int|null  $length  Characters from $start.
162
     * @return  string             Extracted part of the string.
163
     */
164 24
    public static function substr(string $string, int $start, ?int $length = null): string
165
    {
166 24
        return mb_substr($string, $start, $length, (self::$encoding ?? null));
167
    }
168
169
    /**
170
     * lcfirst()
171
     *
172
     * Convert the first character of a given string to lower case.
173
     *
174
     * @since   1.0.1
175
     *
176
     * @param   string  $string  The input string.
177
     * @return  string
178
     */
179 20
    public static function lcfirst(string $string): string
180
    {
181 20
        return self::lower(self::substr($string, 0, 1)) . self::substr($string, 1);
182
    }
183
184
    /**
185
     * ucfirst()
186
     *
187
     * Convert the first character of a given string to upper case.
188
     *
189
     * @since   1.0.1
190
     *
191
     * @param   string  $string  The input string.
192
     * @return  string
193
     */
194 1
    public static function ucfirst(string $string): string
195
    {
196 1
        return self::upper(self::substr($string, 0, 1)) . self::substr($string, 1);
197
    }
198
199
    /**
200
     * Compares multibyte input strings in a binary safe case-insensitive manner.
201
     *
202
     * @since  1.0.1
203
     *
204
     * @param  string  $str1  The first string.
205
     * @param  string  $str2  The second string.
206
     * @return int            Returns < 0 if $str1 is less than $str2; > 0 if $str1
207
     *                        is greater than $str2, and 0 if they are equal.
208
     */
209 1
    public static function strcasecmp(string $str1, string $str2): int
210
    {
211 1
        return strcmp(Strings::upper($str1), Strings::upper($str2));
212
    }
213
214
    /**
215
     * beginsWith()
216
     *
217
     * Determine if a string begins with another string.
218
     *
219
     * @param   string  $haystack     String to search in.
220
     * @param   string  $needle       String to check for.
221
     * @param   bool    $insensitive  True to do a case-insensitive search.
222
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
223
     * @return  bool
224
     */
225 1
    public static function beginsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
226
    {
227 1
        if ($insensitive) {
228 1
            $haystack = Strings::lower($haystack);
229 1
            $needle   = Strings::lower($needle);
230
        }
231 1
        return (
232 1
            $multibyte
233 1
            ? mb_strpos($haystack, $needle) === 0
234 1
            : str_starts_with($haystack, $needle)
235 1
        );
236
    }
237
238
    /**
239
     * endsWith()
240
     *
241
     * Determine if a string ends with another string.
242
     *
243
     * @param   string  $haystack     String to search in.
244
     * @param   string  $needle       String to check for.
245
     * @param   bool    $insensitive  True to do a case-insensitive search.
246
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
247
     * @return  bool
248
     */
249 1
    public static function endsWith(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
250
    {
251 1
        if ($insensitive) {
252 1
            $haystack = Strings::lower($haystack);
253 1
            $needle   = Strings::lower($needle);
254
        }
255 1
        return (
256 1
            $multibyte
257 1
            ? Strings::substr($haystack, -Strings::length($needle)) === $needle
258 1
            : str_ends_with($haystack, $needle)
259 1
        );
260
    }
261
262
    /**
263
     * doesContain()
264
     *
265
     * Determine if a string exists within another string.
266
     *
267
     * @param   string  $haystack     String to search in.
268
     * @param   string  $needle       String to check for.
269
     * @param   bool    $insensitive  True to do a case-insensitive search.
270
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
271
     * @return  bool
272
     */
273 2
    public static function doesContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
274
    {
275 2
        if ($insensitive) {
276 2
            $haystack = Strings::lower($haystack);
277 2
            $needle   = Strings::lower($needle);
278
        }
279 2
        return (
280 2
            $multibyte
281 2
            ? mb_strpos($haystack, $needle) !== false
282 2
            : str_contains($haystack, $needle)
283 2
        );
284
    }
285
286
    /**
287
     * doesNotContain()
288
     *
289
     * Determine if a string does not exist within another string.
290
     *
291
     * @param   string  $haystack     String to search in.
292
     * @param   string  $needle       String to check for.
293
     * @param   bool    $insensitive  True to do a case-insensitive search.
294
     * @param   bool    $multibyte    True to perform checks via mbstring, false otherwise.
295
     * @return  bool
296
     */
297 1
    public static function doesNotContain(string $haystack, string $needle, bool $insensitive = false, bool $multibyte = false): bool
298
    {
299 1
        return (!Strings::doesContain($haystack, $needle, $insensitive, $multibyte));
300
    }
301
302
    /**
303
     * length()
304
     *
305
     * Get string length.
306
     *
307
     * @param   string  $string      The string being checked for length.
308
     * @param   bool    $binarySafe  Forces '8bit' encoding so that the length check is binary safe.
309
     * @return  int
310
     */
311 2
    public static function length(string $string, bool $binarySafe = false): int
312
    {
313 2
        return mb_strlen($string, ($binarySafe ? '8bit' : self::$encoding));
314
    }
315
316
    /**
317
     * camelCase()
318
     *
319
     * Returns a camelCase version of the string.
320
     *
321
     * Shoutout to Daniel St. Jules (https://github.com/danielstjules/Stringy/) for
322
     * inspiration for this function. This function is based on Stringy/camelize().
323
     *
324
     * My changes were mainly to make it fit into Utility.
325
     *
326
     * @since 2.0.0
327
     *
328
     * @param  string  $string  String to camelCase.
329
     * @return string           camelCase'd string.
330
     */
331 19
    public static function camelCase(string $string): string
332
    {
333
        // Trim surrounding spaces
334 19
        $string = trim($string);
335 19
        $string = self::lcfirst($string);
336
337
        // Remove leading dashes and underscores
338 19
        $string = ltrim($string, '-_');
339
340
        // Transformation
341 19
        $transformation = (string) preg_replace_callback(
342 19
            '/[-_\s]+(.)?/u',
343 19
            static fn (array $match): string => isset($match[1]) ? self::upper($match[1]) : '',
344 19
            $string
345 19
        );
346 19
        return (string) preg_replace_callback('/\p{N}+(.)?/u', static fn (array $match): string => self::upper($match[0]), $transformation);
347
    }
348
349
    /**
350
     * ascii()
351
     *
352
     * Transliterate a UTF-8 value to ASCII.
353
     *
354
     * @param   string  $value     Value to transliterate.
355
     * @param   string  $language  Language code (2 characters, eg: en). {@see ASCII}
356
     * @return  string
357
     */
358 3
    public static function ascii(string $value, string $language = 'en'): string
359
    {
360
        /** @var ASCII::*_LANGUAGE_CODE $language */
361 3
        return ASCII::to_ascii($value, $language);
362
    }
363
364
    /**
365
     * slugify()
366
     *
367
     * Transforms a string into a URL or filesystem-friendly string.
368
     *
369
     * Note: Adapted from Illuminate/Support/Str::slug.
370
     *
371
     * @see https://packagist.org/packages/illuminate/support#v6.20.44
372
     * * @see http://opensource.org/licenses/MIT
373
     *
374
     * @param   string   $title      String to convert.
375
     * @param   string   $separator  Separator used to separate words in $title.
376
     * @param   ?string  $language   Language code (2 characters, eg: en). {@see ASCII}
377
     * @return  string
378
     */
379 1
    public static function slugify(string $title, string $separator = '-', ?string $language = 'en'): string
380
    {
381 1
        $title = $language !== null ? Strings::ascii($title) : $title;
382
383
        // Replace @ with the word 'at'
384 1
        $title = str_replace('@', $separator . 'at' . $separator, $title);
385
386 1
        $title = (string) preg_replace('![' . preg_quote(($separator === '-' ? '_' : '-')) . ']+!u', $separator, $title);
387
388
        // Remove all characters that are not the separator, letters, numbers, or whitespace.
389 1
        $title = (string) preg_replace('![^' . preg_quote($separator) . '\pL\pN\s]+!u', '', Strings::lower($title));
390
391
        // Replace all separator characters and whitespace by a single separator
392 1
        $title = (string) preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $title);
393
394
        // Cleanup $title
395 1
        return trim($title, $separator);
396
    }
397
398
    /**
399
     * randomBytes()
400
     *
401
     * Generate cryptographically secure pseudo-random bytes.
402
     *
403
     * @param   int<1, max>  $length  Length of the random string that should be returned in bytes.
404
     * @return  string
405
     *
406
     * @throws RandomException
407
     * @throws ValueError
408
     */
409 2
    public static function randomBytes(int $length): string
410
    {
411
        // Sanity check
412 2
        if ($length < 1) { // @phpstan-ignore-line
413 1
            throw new RandomException('$length must be greater than 1.');
414
        }
415
416
        // Generate bytes
417 2
        return random_bytes($length);
418
    }
419
420
    /**
421
     * randomString()
422
     *
423
     * Generates a secure random string, based on {@see static::randomBytes()}.
424
     *
425
     * @param   int<min, max>  $length  Length the random string should be.
426
     * @return  string
427
     *
428
     * @throws RandomException | ValueError
429
     */
430 1
    public static function randomString(int $length = 8): string
431
    {
432
        // Sanity check
433 1
        if ($length < 1) {
434 1
            throw new RandomException('$length must be greater than 1.');
435
        }
436
        // Convert bytes to hexadecimal and truncate to the desired length
437 1
        return Strings::substr(bin2hex(Strings::randomBytes($length * 2)), 0, $length);
438
    }
439
440
    /**
441
     * validEmail()
442
     *
443
     * Validate an email address using PHP's built-in filter.
444
     *
445
     * @param   string  $email Value to check.
446
     * @return  bool
447
     */
448 2
    public static function validEmail(string $email): bool
449
    {
450 2
        return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
451
    }
452
453
    /**
454
     * validJson()
455
     *
456
     * Determines if a string is valid JSON.
457
     *
458
     * @deprecated as of 2.0.0
459
     *
460
     * @param   string  $data   The string to validate as JSON.
461
     * @return  bool
462
     */
463 1
    public static function validJson(string $data): bool
464
    {
465 1
        $data = trim($data);
466 1
        json_decode($data);
467
468 1
        return (json_last_error() === JSON_ERROR_NONE);
469
    }
470
471
    /**
472
     * obscureEmail()
473
     *
474
     * Obscures an email address.
475
     *
476
     * @param   string  $email  Email address to obscure.
477
     * @return  string          Obscured email address.
478
     *
479
     * @throws  InvalidArgumentException
480
     */
481 1
    public static function obscureEmail(string $email): string
482
    {
483
        // Sanity check
484 1
        if (!Strings::validEmail($email)) {
485 1
            throw new InvalidArgumentException('Invalid $email specified.');
486
        }
487
488
        // Split and process
489 1
        $email = array_map(
490 1
            static fn (string $char): string => '&#' . ord($char) . ';',
491
            /** @scrutinizer ignore-type */
492 1
            str_split($email)
493 1
        );
494
495 1
        return implode('', $email);
496
    }
497
498
    /**
499
     * guid()
500
     *
501
     * Generate a Globally/Universally Unique Identifier (version 4).
502
     *
503
     * @return  string
504
     *
505
     * @throws RandomException
506
     */
507 1
    public static function guid(): string
508
    {
509 1
        return sprintf(
510 1
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
511 1
            Numbers::random(0, 0xffff),
512 1
            Numbers::random(0, 0xffff),
513 1
            Numbers::random(0, 0xffff),
514 1
            Numbers::random(0, 0x0fff) | 0x4000,
515 1
            Numbers::random(0, 0x3fff) | 0x8000,
516 1
            Numbers::random(0, 0xffff),
517 1
            Numbers::random(0, 0xffff),
518 1
            Numbers::random(0, 0xffff)
519 1
        );
520
    }
521
}
522