Passed
Pull Request — master (#116)
by Sergei
02:22
created

StringHelper::implode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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

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

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