StringHelper   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 700
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 152
c 4
b 0
f 0
dl 0
loc 700
ccs 192
cts 192
cp 1
rs 2.88
wmc 69

31 Methods

Rating   Name   Duplication   Size   Complexity  
A rtrim() 0 5 1
A ltrim() 0 5 1
A trim() 0 5 1
A startsWith() 0 3 2
B replaceSubstring() 0 28 8
A uppercaseFirstCharacterInEachWord() 0 10 1
A byteLength() 0 3 1
A substring() 0 3 1
A directoryName() 0 8 2
A length() 0 3 1
A endsWith() 0 3 2
A split() 0 4 1
A endsWithIgnoringCase() 0 10 2
A truncateMiddle() 0 13 2
A truncateBegin() 0 10 2
A uppercaseFirstCharacter() 0 6 1
A baseName() 0 13 4
A uppercase() 0 3 1
A startsWithIgnoringCase() 0 10 2
A byteSubstring() 0 3 1
A countWords() 0 5 1
A truncateWords() 0 10 2
A lowercase() 0 3 1
A truncateEnd() 0 10 2
A base64UrlDecode() 0 3 1
A base64UrlEncode() 0 3 1
A findBetweenFirst() 0 20 4
A findBetweenLast() 0 21 4
A findBetween() 0 20 4
A ensureUtf8Pattern() 0 4 2
B parsePath() 0 66 10

How to fix   Complexity   

Complex Class

Complex classes like StringHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StringHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Strings;
6
7
use InvalidArgumentException;
8
9
use function array_map;
10
use function array_slice;
11
use function base64_decode;
12
use function base64_encode;
13
use function ceil;
14
use function count;
15
use function implode;
16
use function max;
17
use function mb_strlen;
18
use function mb_strrpos;
19
use function mb_strtolower;
20
use function mb_strtoupper;
21
use function mb_substr;
22
use function preg_match;
23
use function preg_quote;
24
use function preg_replace;
25
use function preg_split;
26
use function rtrim;
27
use function sprintf;
28
use function str_ends_with;
29
use function str_repeat;
30
use function str_replace;
31
use function str_starts_with;
32
use function strlen;
33
use function strtr;
34
use function trim;
35
36
/**
37
 * Provides static methods to work with strings.
38
 */
39
final class StringHelper
40
{
41
    public const DEFAULT_WHITESPACE_PATTERN = "\pC\pZ";
42
43
    /**
44
     * Returns the number of bytes in the given string.
45
     * This method ensures the string is treated as a byte array even if `mbstring.func_overload` is turned on
46
     * by using {@see mb_strlen()}.
47
     *
48
     * @param string|null $input The string being measured for length.
49
     *
50
     * @return int The number of bytes in the given string.
51
     */
52 2
    public static function byteLength(string|null $input): int
53
    {
54 2
        return mb_strlen((string)$input, '8bit');
55
    }
56
57
    /**
58
     * Returns the portion of string specified by the start and length parameters.
59
     * This method ensures the string is treated as a byte array by using `mb_substr()`.
60
     *
61
     * @param string $input The input string. Must be one character or longer.
62
     * @param int $start The starting position.
63
     * @param int|null $length The desired portion length. If not specified or `null`, there will be
64
     * no limit on length i.e. the output will be until the end of the string.
65
     *
66
     * @return string The extracted part of string, or FALSE on failure or an empty string.
67
     *
68
     * @see https://www.php.net/manual/en/function.substr.php
69
     */
70 1
    public static function byteSubstring(string $input, int $start, int $length = null): string
71
    {
72 1
        return mb_substr($input, $start, $length ?? mb_strlen($input, '8bit'), '8bit');
73
    }
74
75
    /**
76
     * Returns the trailing name component of a path.
77
     * This method is similar to the php function `basename()` except that it will
78
     * treat both \ and / as directory separators, independent of the operating system.
79
     * This method was mainly created to work on php namespaces. When working with real
80
     * file paths, PHP's `basename()` should work fine for you.
81
     * Note: this method is not aware of the actual filesystem, or path components such as "..".
82
     *
83
     * @param string $path A path string.
84
     * @param string $suffix If the name component ends in suffix this will also be cut off.
85
     *
86
     * @return string The trailing name component of the given path.
87
     *
88
     * @see https://www.php.net/manual/en/function.basename.php
89
     */
90 1
    public static function baseName(string $path, string $suffix = ''): string
91
    {
92 1
        $length = mb_strlen($suffix);
93 1
        if ($length > 0 && mb_substr($path, -$length) === $suffix) {
94 1
            $path = mb_substr($path, 0, -$length);
95
        }
96 1
        $path = rtrim(str_replace('\\', '/', $path), '/\\');
97 1
        $position = mb_strrpos($path, '/');
98 1
        if ($position !== false) {
99 1
            return mb_substr($path, $position + 1);
100
        }
101
102 1
        return $path;
103
    }
104
105
    /**
106
     * Returns parent directory's path.
107
     * This method is similar to `dirname()` except that it will treat
108
     * both \ and / as directory separators, independent of the operating system.
109
     *
110
     * @param string $path A path string.
111
     *
112
     * @return string The parent directory's path.
113
     *
114
     * @see https://www.php.net/manual/en/function.basename.php
115
     */
116 1
    public static function directoryName(string $path): string
117
    {
118 1
        $position = mb_strrpos(str_replace('\\', '/', $path), '/');
119 1
        if ($position !== false) {
120 1
            return mb_substr($path, 0, $position);
121
        }
122
123 1
        return '';
124
    }
125
126
    /**
127
     * Get part of string.
128
     *
129
     * @param string $string To get substring from.
130
     * @param int $start Character to start at.
131
     * @param int|null $length Number of characters to get.
132
     * @param string $encoding The encoding to use, defaults to "UTF-8".
133
     *
134
     * @see https://php.net/manual/en/function.mb-substr.php
135
     */
136 15
    public static function substring(string $string, int $start, int $length = null, string $encoding = 'UTF-8'): string
137
    {
138 15
        return mb_substr($string, $start, $length, $encoding);
139
    }
140
141
    /**
142
     * Replace text within a portion of a string.
143
     *
144
     * @param string $string The input string.
145
     * @param string $replacement The replacement string.
146
     * @param int $start Position to begin replacing substring at.
147
     * If start is non-negative, the replacing will begin at the start'th offset into string.
148
     * If start is negative, the replacing will begin at the start'th character from the end of string.
149
     * @param int|null $length Length of the substring to be replaced.
150
     * If given and is positive, it represents the length of the portion of string which is to be replaced.
151
     * If it is negative, it represents the number of characters from the end of string at which to stop replacing.
152
     * If it is not given, then it will default to the length of the string; i.e. end the replacing at the end of string.
153
     * If length is zero then this function will have the effect of inserting replacement into string at the given start offset.
154
     * @param string $encoding The encoding to use, defaults to "UTF-8".
155
     */
156 9
    public static function replaceSubstring(
157
        string $string,
158
        string $replacement,
159
        int $start,
160
        int|null $length = null,
161
        string $encoding = 'UTF-8',
162
    ): string {
163 9
        $stringLength = mb_strlen($string, $encoding);
164
165 9
        if ($start < 0) {
166 2
            $start = max(0, $stringLength + $start);
167 7
        } elseif ($start > $stringLength) {
168 1
            $start = $stringLength;
169
        }
170
171 9
        if ($length !== null && $length < 0) {
172 3
            $length = max(0, $stringLength - $start + $length);
173 6
        } elseif ($length === null || $length > $stringLength) {
174 5
            $length = $stringLength;
175
        }
176
177 9
        if (($start + $length) > $stringLength) {
178 4
            $length = $stringLength - $start;
179
        }
180
181 9
        return mb_substr($string, 0, $start, $encoding)
182 9
            . $replacement
183 9
            . mb_substr($string, $start + $length, $stringLength - $start - $length, $encoding);
184
    }
185
186
    /**
187
     * Check if given string starts with specified substring.
188
     * Binary and multibyte safe.
189
     *
190
     * @param string $input Input string.
191
     * @param string|null $with Part to search inside the $string.
192
     *
193
     * @return bool Returns true if first input starts with second input, false otherwise.
194
     */
195 19
    public static function startsWith(string $input, string|null $with): bool
196
    {
197 19
        return $with === null || str_starts_with($input, $with);
198
    }
199
200
    /**
201
     * Check if given string starts with specified substring ignoring case.
202
     * Binary and multibyte safe.
203
     *
204
     * @param string $input Input string.
205
     * @param string|null $with Part to search inside the $string.
206
     *
207
     * @return bool Returns true if first input starts with second input, false otherwise.
208
     */
209 1
    public static function startsWithIgnoringCase(string $input, string|null $with): bool
210
    {
211 1
        $bytes = self::byteLength($with);
212
213 1
        if ($bytes === 0) {
214 1
            return true;
215
        }
216
217
        /** @psalm-suppress PossiblyNullArgument */
218 1
        return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase($with);
0 ignored issues
show
Bug introduced by
It seems like $with can also be of type null; however, parameter $string of Yiisoft\Strings\StringHelper::lowercase() does only seem to accept string, 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

218
        return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
219
    }
220
221
    /**
222
     * Check if given string ends with specified substring.
223
     * Binary and multibyte safe.
224
     *
225
     * @param string $input Input string to check.
226
     * @param string|null $with Part to search inside of the $string.
227
     *
228
     * @return bool Returns true if first input ends with second input, false otherwise.
229
     */
230 19
    public static function endsWith(string $input, string|null $with): bool
231
    {
232 19
        return $with === null || str_ends_with($input, $with);
233
    }
234
235
    /**
236
     * Check if given string ends with specified substring.
237
     * Binary and multibyte safe.
238
     *
239
     * @param string $input Input string to check.
240
     * @param string|null $with Part to search inside of the $string.
241
     *
242
     * @return bool Returns true if first input ends with second input, false otherwise.
243
     */
244 1
    public static function endsWithIgnoringCase(string $input, string|null $with): bool
245
    {
246 1
        $bytes = self::byteLength($with);
247
248 1
        if ($bytes === 0) {
249 1
            return true;
250
        }
251
252
        /** @psalm-suppress PossiblyNullArgument */
253 1
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase($with);
0 ignored issues
show
Bug introduced by
It seems like $with can also be of type null; however, parameter $string of Yiisoft\Strings\StringHelper::lowercase() does only seem to accept string, 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

253
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
254
    }
255
256
    /**
257
     * Truncates a string from the beginning to the number of characters specified.
258
     *
259
     * @param string $input String to process.
260
     * @param int $length Maximum length of the truncated string including trim marker.
261
     * @param string $trimMarker String to append to the beginning.
262
     * @param string $encoding The encoding to use, defaults to "UTF-8".
263
     */
264 1
    public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
265
    {
266 1
        $inputLength = mb_strlen($input, $encoding);
267
268 1
        if ($inputLength <= $length) {
269 1
            return $input;
270
        }
271
272 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
273 1
        return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
274
    }
275
276
    /**
277
     * Truncates a string in the middle. Keeping start and end.
278
     * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
279
     *
280
     * @param string $input The string to truncate.
281
     * @param int $length Maximum length of the truncated string including trim marker.
282
     * @param string $trimMarker String to append in the middle of truncated string.
283
     * @param string $encoding The encoding to use, defaults to "UTF-8".
284
     *
285
     * @return string The truncated string.
286
     */
287 2
    public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
288
    {
289 2
        $inputLength = mb_strlen($input, $encoding);
290
291 2
        if ($inputLength <= $length) {
292 1
            return $input;
293
        }
294
295 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
296 1
        $start = (int)ceil(($length - $trimMarkerLength) / 2);
297 1
        $end = $length - $start - $trimMarkerLength;
298
299 1
        return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
300
    }
301
302
    /**
303
     * Truncates a string from the end to the number of characters specified.
304
     *
305
     * @param string $input The string to truncate.
306
     * @param int $length Maximum length of the truncated string including trim marker.
307
     * @param string $trimMarker String to append to the end of truncated string.
308
     * @param string $encoding The encoding to use, defaults to "UTF-8".
309
     *
310
     * @return string The truncated string.
311
     */
312 1
    public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
313
    {
314 1
        $inputLength = mb_strlen($input, $encoding);
315
316 1
        if ($inputLength <= $length) {
317 1
            return $input;
318
        }
319
320 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
321 1
        return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
322
    }
323
324
    /**
325
     * Truncates a string to the number of words specified.
326
     *
327
     * @param string $input The string to truncate.
328
     * @param int $count How many words from original string to include into truncated string.
329
     * @param string $trimMarker String to append to the end of truncated string.
330
     *
331
     * @return string The truncated string.
332
     */
333 1
    public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
334
    {
335
        /** @psalm-var list<string> $words */
336 1
        $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
337 1
        if (count($words) / 2 > $count) {
338 1
            $words = array_slice($words, 0, ($count * 2) - 1);
339 1
            return implode('', $words) . $trimMarker;
340
        }
341
342 1
        return $input;
343
    }
344
345
    /**
346
     * Get string length.
347
     *
348
     * @param string $string String to calculate length for.
349
     * @param string $encoding The encoding to use, defaults to "UTF-8".
350
     *
351
     * @see https://php.net/manual/en/function.mb-strlen.php
352
     */
353 1
    public static function length(string $string, string $encoding = 'UTF-8'): int
354
    {
355 1
        return mb_strlen($string, $encoding);
356
    }
357
358
    /**
359
     * Counts words in a string.
360
     */
361 1
    public static function countWords(string $input): int
362
    {
363
        /** @var array $words */
364 1
        $words = preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY);
365 1
        return count($words);
366
    }
367
368
    /**
369
     * Make a string lowercase.
370
     *
371
     * @param string $string String to process.
372
     * @param string $encoding The encoding to use, defaults to "UTF-8".
373
     *
374
     * @see https://php.net/manual/en/function.mb-strtolower.php
375
     */
376 3
    public static function lowercase(string $string, string $encoding = 'UTF-8'): string
377
    {
378 3
        return mb_strtolower($string, $encoding);
379
    }
380
381
    /**
382
     * Make a string uppercase.
383
     *
384
     * @param string $string String to process.
385
     * @param string $encoding The encoding to use, defaults to "UTF-8".
386
     *
387
     * @see https://php.net/manual/en/function.mb-strtoupper.php
388
     */
389 15
    public static function uppercase(string $string, string $encoding = 'UTF-8'): string
390
    {
391 15
        return mb_strtoupper($string, $encoding);
392
    }
393
394
    /**
395
     * Make a string's first character uppercase.
396
     *
397
     * @param string $string The string to be processed.
398
     * @param string $encoding The encoding to use, defaults to "UTF-8".
399
     *
400
     * @see https://php.net/manual/en/function.ucfirst.php
401
     */
402 14
    public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
403
    {
404 14
        $firstCharacter = self::substring($string, 0, 1, $encoding);
405 14
        $rest = self::substring($string, 1, null, $encoding);
406
407 14
        return self::uppercase($firstCharacter, $encoding) . $rest;
408
    }
409
410
    /**
411
     * Uppercase the first character of each word in a string.
412
     *
413
     * @param string $string The string to be processed.
414
     * @param string $encoding The encoding to use, defaults to "UTF-8".
415
     *
416
     * @see https://php.net/manual/en/function.ucwords.php
417
     */
418 10
    public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
419
    {
420 10
        $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY);
421
422 10
        $wordsWithUppercaseFirstCharacter = array_map(
423 10
            static fn (string $word) => self::uppercaseFirstCharacter($word, $encoding),
424 10
            $words
425 10
        );
426
427 10
        return implode(' ', $wordsWithUppercaseFirstCharacter);
428
    }
429
430
    /**
431
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
432
     *
433
     * > Note: Base 64 padding `=` may be at the end of the returned string.
434
     * > `=` is not transparent to URL encoding.
435
     *
436
     * @see https://tools.ietf.org/html/rfc4648#page-7
437
     *
438
     * @param string $input The string to encode.
439
     *
440
     * @return string Encoded string.
441
     */
442 4
    public static function base64UrlEncode(string $input): string
443
    {
444 4
        return strtr(base64_encode($input), '+/', '-_');
445
    }
446
447
    /**
448
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
449
     *
450
     * @see https://tools.ietf.org/html/rfc4648#page-7
451
     *
452
     * @param string $input Encoded string.
453
     *
454
     * @return string Decoded string.
455
     */
456 4
    public static function base64UrlDecode(string $input): string
457
    {
458 4
        return base64_decode(strtr($input, '-_', '+/'));
459
    }
460
461
    /**
462
     * Split a string to array with non-empty lines.
463
     * Whitespace from the beginning and end of a each line will be stripped.
464
     *
465
     * @param string $string The input string.
466
     * @param string $separator The boundary string. It is a part of regular expression
467
     * so should be taken into account or properly escaped with {@see preg_quote()}.
468
     */
469 16
    public static function split(string $string, string $separator = '\R'): array
470
    {
471 16
        $string = preg_replace('(^\s*|\s*$)', '', $string);
472 16
        return preg_split('~\s*' . $separator . '\s*~u', $string, -1, PREG_SPLIT_NO_EMPTY);
473
    }
474
475
    /**
476
     * @param string $path The path of where do you want to write a value to `$array`. The path can be described by
477
     * a string when each key should be separated by delimiter. If a path item contains delimiter, it can be escaped
478
     * with "\" (backslash) or a custom delimiter can be used.
479
     * @param string $delimiter A separator, used to parse string key for embedded object property retrieving. Defaults
480
     * to "." (dot).
481
     * @param string $escapeCharacter An escape character, used to escape delimiter. Defaults to "\" (backslash).
482
     * @param bool $preserveDelimiterEscaping Whether to preserve delimiter escaping in the items of final array (in
483
     * case of using string as an input). When `false`, "\" (backslashes) are removed. For a "." as delimiter, "."
484
     * becomes "\.". Defaults to `false`.
485
     *
486
     * @return string[]
487
     *
488
     * @psalm-return list<string>
489
     */
490 35
    public static function parsePath(
491
        string $path,
492
        string $delimiter = '.',
493
        string $escapeCharacter = '\\',
494
        bool $preserveDelimiterEscaping = false
495
    ): array {
496 35
        if (strlen($delimiter) !== 1) {
497 1
            throw new InvalidArgumentException('Only 1 character is allowed for delimiter.');
498
        }
499
500 34
        if (strlen($escapeCharacter) !== 1) {
501 1
            throw new InvalidArgumentException('Only 1 escape character is allowed.');
502
        }
503
504 33
        if ($delimiter === $escapeCharacter) {
505 1
            throw new InvalidArgumentException('Delimiter and escape character must be different.');
506
        }
507
508 32
        if ($path === '') {
509 2
            return [];
510
        }
511
512 30
        if (!str_contains($path, $delimiter)) {
513 3
            if ($preserveDelimiterEscaping) {
514 1
                return [$path];
515
            }
516
517 2
            return [str_replace($escapeCharacter . $escapeCharacter, $escapeCharacter, $path)];
518
        }
519
520
        /** @psalm-var non-empty-list<array{0:string, 1:int}> $matches */
521 27
        $matches = preg_split(
522 27
            sprintf(
523 27
                '/(?<!%1$s)((?>%1$s%1$s)*)%2$s/',
524 27
                preg_quote($escapeCharacter, '/'),
525 27
                preg_quote($delimiter, '/')
526 27
            ),
527 27
            $path,
528 27
            -1,
529 27
            PREG_SPLIT_OFFSET_CAPTURE
530 27
        );
531 27
        $result = [];
532 27
        $countResults = count($matches);
533 27
        for ($i = 1; $i < $countResults; $i++) {
534 25
            $l = $matches[$i][1] - $matches[$i - 1][1] - strlen($matches[$i - 1][0]) - 1;
535 25
            $result[] = $matches[$i - 1][0] . ($l > 0 ? str_repeat($escapeCharacter, $l) : '');
536
        }
537 27
        $result[] = $matches[$countResults - 1][0];
538
539 27
        if ($preserveDelimiterEscaping === true) {
540 1
            return $result;
541
        }
542
543 26
        return array_map(
544 26
            static fn (string $key): string => str_replace(
545 26
                [
546 26
                    $escapeCharacter . $escapeCharacter,
547 26
                    $escapeCharacter . $delimiter,
548 26
                ],
549 26
                [
550 26
                    $escapeCharacter,
551 26
                    $delimiter,
552 26
                ],
553 26
                $key
554 26
            ),
555 26
            $result
556 26
        );
557
    }
558
559
    /**
560
     * Strip Unicode whitespace (with Unicode symbol property White_Space=yes) or other characters from the beginning and end of a string.
561
     * Input string and pattern are treated as UTF-8.
562
     *
563
     * @see https://en.wikipedia.org/wiki/Whitespace_character#Unicode
564
     * @see https://www.php.net/manual/function.preg-replace
565
     *
566
     * @param string|string[] $string The string or an array with strings.
567
     * @param string $pattern PCRE regex pattern to search for, as UTF-8 string. Use {@see preg_quote()} to quote `$pattern` if it contains
568
     * special regular expression characters.
569
     *
570
     * @psalm-template TKey of array-key
571
     * @psalm-param string|array<TKey, string> $string
572
     * @psalm-param non-empty-string $pattern
573
     * @psalm-return ($string is array ? array<TKey, string> : string)
574
     *
575
     * @return string|string[]
576
     */
577 16
    public static function trim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array
578
    {
579 16
        self::ensureUtf8Pattern($pattern);
580
581 15
        return preg_replace("#^[$pattern]+|[$pattern]+$#uD", '', $string);
582
    }
583
584
    /**
585
     * Strip Unicode whitespace (with Unicode symbol property White_Space=yes) or other characters from the beginning of a string.
586
     *
587
     * @see self::trim()
588
     *
589
     * @param string|string[] $string The string or an array with strings.
590
     * @param string $pattern PCRE regex pattern to search for, as UTF-8 string. Use {@see preg_quote()} to quote `$pattern` if it contains
591
     * special regular expression characters.
592
     *
593
     * @psalm-template TKey of array-key
594
     * @psalm-param string|array<TKey, string> $string
595
     * @psalm-param non-empty-string $pattern
596
     * @psalm-return ($string is array ? array<TKey, string> : string)
597
     *
598
     * @return string|string[]
599
     */
600 12
    public static function ltrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array
601
    {
602 12
        self::ensureUtf8Pattern($pattern);
603
604 12
        return preg_replace("#^[$pattern]+#u", '', $string);
605
    }
606
607
    /**
608
     * Strip Unicode whitespace (with Unicode symbol property White_Space=yes) or other characters from the end of a string.
609
     *
610
     * @see self::trim()
611
     *
612
     * @param string|string[] $string The string or an array with strings.
613
     * @param string $pattern PCRE regex pattern to search for, as UTF-8 string. Use {@see preg_quote()} to quote `$pattern` if it contains
614
     * special regular expression characters.
615
     *
616
     * @psalm-template TKey of array-key
617
     * @psalm-param string|array<TKey, string> $string
618
     * @psalm-param non-empty-string $pattern
619
     * @psalm-return ($string is array ? array<TKey, string> : string)
620
     *
621
     * @return string|string[]
622
     */
623 14
    public static function rtrim(string|array $string, string $pattern = self::DEFAULT_WHITESPACE_PATTERN): string|array
624
    {
625 14
        self::ensureUtf8Pattern($pattern);
626
627 14
        return preg_replace("#[$pattern]+$#uD", '', $string);
628
    }
629
630
    /**
631
     * Returns the portion of the string that lies between the first occurrence of the `$start` string
632
     * and the last occurrence of the `$end` string after that.
633
     *
634
     * @param string $string The input string.
635
     * @param string $start The string marking the start of the portion to extract.
636
     * @param string|null $end The string marking the end of the portion to extract.
637
     * If the `$end` string is not provided, it defaults to the value of the `$start` string.
638
     * @return string|null The portion of the string between the first occurrence of
639
     * `$start` and the last occurrence of `$end`, or null if either `$start` or `$end` cannot be found.
640
     */
641 14
    public static function findBetween(string $string, string $start, ?string $end = null): ?string
642
    {
643 14
        if ($end === null) {
644 1
            $end = $start;
645
        }
646
647 14
        $startPos = mb_strpos($string, $start);
648
649 14
        if ($startPos === false) {
650 3
            return null;
651
        }
652
653 11
        $startPos += mb_strlen($start);
654 11
        $endPos = mb_strrpos($string, $end, $startPos);
655
656 11
        if ($endPos === false) {
657 2
            return null;
658
        }
659
660 9
        return mb_substr($string, $startPos, $endPos - $startPos);
661
    }
662
663
    /**
664
     * Returns the portion of the string between the initial occurrence of the '$start' string
665
     * and the next occurrence of the '$end' string.
666
     *
667
     * @param string $string The input string.
668
     * @param string $start The string marking the beginning of the segment to extract.
669
     * @param string|null $end The string marking the termination of the segment.
670
     * If the '$end' string is not provided, it defaults to the value of the '$start' string.
671
     * @return string|null Extracted segment, or null if '$start' or '$end' is not present.
672
     */
673 16
    public static function findBetweenFirst(string $string, string $start, ?string $end = null): ?string
674
    {
675 16
        if ($end === null) {
676 1
            $end = $start;
677
        }
678
679 16
        $startPos = mb_strpos($string, $start);
680
681 16
        if ($startPos === false) {
682 3
            return null;
683
        }
684
685 13
        $startPos += mb_strlen($start);
686 13
        $endPos = mb_strpos($string, $end, $startPos);
687
688 13
        if ($endPos === false) {
689 2
            return null;
690
        }
691
692 11
        return mb_substr($string, $startPos, $endPos - $startPos);
693
    }
694
695
    /**
696
     * Returns the portion of the string between the latest '$start' string
697
     * and the subsequent '$end' string.
698
     *
699
     * @param string $string The input string.
700
     * @param string $start The string marking the beginning of the segment to extract.
701
     * @param string|null $end The string marking the termination of the segment.
702
     * If the '$end' string is not provided, it defaults to the value of the '$start' string.
703
     * @return string|null Extracted segment, or null if '$start' or '$end' is not present.
704
     */
705 16
    public static function findBetweenLast(string $string, string $start, ?string $end = null): ?string
706
    {
707 16
        if ($end === null) {
708 1
            $end = $start;
709
        }
710
711 16
        $endPos = mb_strrpos($string, $end);
712
713 16
        if ($endPos === false) {
714 3
            return null;
715
        }
716
717 13
        $startPos = mb_strrpos(mb_substr($string, 0, $endPos), $start);
718
719 13
        if ($startPos === false) {
720 2
            return null;
721
        }
722
723 11
        $startPos += mb_strlen($start);
724
725 11
        return mb_substr($string, $startPos, $endPos - $startPos);
726
    }
727
728
    /**
729
     * Ensure the input string is a valid UTF-8 string.
730
     *
731
     * @param string $pattern The input string.
732
     *
733
     * @throws InvalidArgumentException
734
     */
735 42
    private static function ensureUtf8Pattern(string $pattern): void
736
    {
737 42
        if (!preg_match('##u', $pattern)) {
738 1
            throw new InvalidArgumentException('Pattern is not a valid UTF-8 string.');
739
        }
740
    }
741
}
742