Passed
Pull Request — master (#81)
by Sergei
11:46
created

StringHelper::parsePath()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 59
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 32
nc 10
nop 4
dl 0
loc 59
ccs 27
cts 27
cp 1
crap 8
rs 8.1635
c 0
b 0
f 0

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

211
        return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
212
    }
213
214
    /**
215
     * Check if given string ends with specified substring.
216
     * Binary and multibyte safe.
217
     *
218
     * @param string $input Input string to check.
219
     * @param string|null $with Part to search inside of the $string.
220
     *
221
     * @return bool Returns true if first input ends with second input, false otherwise.
222
     */
223 19
    public static function endsWith(string $input, ?string $with): bool
224
    {
225 19
        if ($with === null) {
226 1
            return true;
227
        }
228
229 18
        if (function_exists('\str_ends_with')) {
230 18
            return str_ends_with($input, $with);
231
        }
232
233
        $bytes = self::byteLength($with);
234
        if ($bytes === 0) {
235
            return true;
236
        }
237
238
        // Warning check, see http://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues
239
        if (self::byteLength($input) < $bytes) {
240
            return false;
241
        }
242
243
        return substr_compare($input, $with, -$bytes, $bytes) === 0;
244
    }
245
246
    /**
247
     * Check if given string ends with specified substring.
248
     * Binary and multibyte safe.
249
     *
250
     * @param string $input Input string to check.
251
     * @param string|null $with Part to search inside of the $string.
252
     *
253
     * @return bool Returns true if first input ends with second input, false otherwise.
254
     */
255 1
    public static function endsWithIgnoringCase(string $input, ?string $with): bool
256
    {
257 1
        $bytes = self::byteLength($with);
258 1
        if ($bytes === 0) {
259 1
            return true;
260
        }
261
262
        /**
263
         * @psalm-suppress PossiblyNullArgument
264
         */
265 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

265
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
266
    }
267
268
    /**
269
     * Truncates a string from the beginning to the number of characters specified.
270
     *
271
     * @param string $input String to process.
272
     * @param int $length Maximum length of the truncated string including trim marker.
273
     * @param string $trimMarker String to append to the beginning.
274
     * @param string $encoding The encoding to use, defaults to "UTF-8".
275
     *
276
     * @return string
277
     */
278 1
    public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
279
    {
280 1
        $inputLength = mb_strlen($input, $encoding);
281
282 1
        if ($inputLength <= $length) {
283 1
            return $input;
284
        }
285
286 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
287 1
        return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
288
    }
289
290
    /**
291
     * Truncates a string in the middle. Keeping start and end.
292
     * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
293
     *
294
     * @param string $input The string to truncate.
295
     * @param int $length Maximum length of the truncated string including trim marker.
296
     * @param string $trimMarker String to append in the middle of truncated string.
297
     * @param string $encoding The encoding to use, defaults to "UTF-8".
298
     *
299
     * @return string The truncated string.
300
     */
301 2
    public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
302
    {
303 2
        $inputLength = mb_strlen($input, $encoding);
304
305 2
        if ($inputLength <= $length) {
306 1
            return $input;
307
        }
308
309 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
310 1
        $start = (int)ceil(($length - $trimMarkerLength) / 2);
311 1
        $end = $length - $start - $trimMarkerLength;
312
313 1
        return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
314
    }
315
316
    /**
317
     * Truncates a string from the end to the number of characters specified.
318
     *
319
     * @param string $input The string to truncate.
320
     * @param int $length Maximum length of the truncated string including trim marker.
321
     * @param string $trimMarker String to append to the end of truncated string.
322
     * @param string $encoding The encoding to use, defaults to "UTF-8".
323
     *
324
     * @return string The truncated string.
325
     */
326 1
    public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
327
    {
328 1
        $inputLength = mb_strlen($input, $encoding);
329
330 1
        if ($inputLength <= $length) {
331 1
            return $input;
332
        }
333
334 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
335 1
        return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
336
    }
337
338
    /**
339
     * Truncates a string to the number of words specified.
340
     *
341
     * @param string $input The string to truncate.
342
     * @param int $count How many words from original string to include into truncated string.
343
     * @param string $trimMarker String to append to the end of truncated string.
344
     *
345
     * @return string The truncated string.
346
     */
347 1
    public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
348
    {
349 1
        $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
350 1
        if (count($words) / 2 > $count) {
351
            /** @var string[] $words */
352 1
            $words = array_slice($words, 0, ($count * 2) - 1);
353 1
            return implode('', $words) . $trimMarker;
354
        }
355
356 1
        return $input;
357
    }
358
359
    /**
360
     * Get string length.
361
     *
362
     * @param string $string String to calculate length for.
363
     * @param string $encoding The encoding to use, defaults to "UTF-8".
364
     *
365
     * @see https://php.net/manual/en/function.mb-strlen.php
366
     *
367
     * @return int
368
     */
369 1
    public static function length(string $string, string $encoding = 'UTF-8'): int
370
    {
371 1
        return mb_strlen($string, $encoding);
372
    }
373
374
    /**
375
     * Counts words in a string.
376
     *
377
     * @param string $input
378
     *
379
     * @return int
380
     */
381 1
    public static function countWords(string $input): int
382
    {
383 1
        return count(preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY));
384
    }
385
386
    /**
387
     * Make a string lowercase.
388
     *
389
     * @param string $string String to process.
390
     * @param string $encoding The encoding to use, defaults to "UTF-8".
391
     *
392
     * @see https://php.net/manual/en/function.mb-strtolower.php
393
     *
394
     * @return string
395
     */
396 3
    public static function lowercase(string $string, string $encoding = 'UTF-8'): string
397
    {
398 3
        return mb_strtolower($string, $encoding);
399
    }
400
401
    /**
402
     * Make a string uppercase.
403
     *
404
     * @param string $string String to process.
405
     * @param string $encoding The encoding to use, defaults to "UTF-8".
406
     *
407
     * @see https://php.net/manual/en/function.mb-strtoupper.php
408
     *
409
     * @return string
410
     */
411 15
    public static function uppercase(string $string, string $encoding = 'UTF-8'): string
412
    {
413 15
        return mb_strtoupper($string, $encoding);
414
    }
415
416
    /**
417
     * Make a string's first character uppercase.
418
     *
419
     * @param string $string The string to be processed.
420
     * @param string $encoding The encoding to use, defaults to "UTF-8".
421
     *
422
     * @return string
423
     *
424
     * @see https://php.net/manual/en/function.ucfirst.php
425
     */
426 14
    public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
427
    {
428 14
        $firstCharacter = self::substring($string, 0, 1, $encoding);
429 14
        $rest = self::substring($string, 1, null, $encoding);
430
431 14
        return self::uppercase($firstCharacter, $encoding) . $rest;
432
    }
433
434
    /**
435
     * Uppercase the first character of each word in a string.
436
     *
437
     * @param string $string The string to be processed.
438
     * @param string $encoding The encoding to use, defaults to "UTF-8".
439
     *
440
     * @see https://php.net/manual/en/function.ucwords.php
441
     *
442
     * @return string
443
     */
444 10
    public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
445
    {
446 10
        $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY);
447
448 10
        $wordsWithUppercaseFirstCharacter = array_map(static function (string $word) use ($encoding) {
449 9
            return self::uppercaseFirstCharacter($word, $encoding);
450
        }, $words);
451
452 10
        return implode(' ', $wordsWithUppercaseFirstCharacter);
453
    }
454
455
    /**
456
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
457
     *
458
     * > Note: Base 64 padding `=` may be at the end of the returned string.
459
     * > `=` is not transparent to URL encoding.
460
     *
461
     * @see https://tools.ietf.org/html/rfc4648#page-7
462
     *
463
     * @param string $input The string to encode.
464
     *
465
     * @return string Encoded string.
466
     */
467 4
    public static function base64UrlEncode(string $input): string
468
    {
469 4
        return strtr(base64_encode($input), '+/', '-_');
470
    }
471
472
    /**
473
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
474
     *
475
     * @see https://tools.ietf.org/html/rfc4648#page-7
476
     *
477
     * @param string $input Encoded string.
478
     *
479
     * @return string Decoded string.
480
     */
481 4
    public static function base64UrlDecode(string $input): string
482
    {
483 4
        return base64_decode(strtr($input, '-_', '+/'));
484
    }
485
486
    /**
487
     * Split a string to array with non-empty lines.
488
     * Whitespace from the beginning and end of a each line will be stripped.
489
     *
490
     * @param string $string The input string.
491
     * @param string $separator The boundary string. It is a part of regular expression
492
     * so should be taken into account or properly escaped with {@see preg_quote()}.
493
     *
494
     * @return array
495
     */
496 15
    public static function split(string $string, string $separator = '\R'): array
497
    {
498 15
        $string = preg_replace('(^\s*|\s*$)', '', $string);
499 15
        return preg_split('~\s*' . $separator . '\s*~', $string, -1, PREG_SPLIT_NO_EMPTY);
500
    }
501
502
    /**
503
     * @param string $path The path of where do you want to write a value to `$array`. The path can be described by
504
     * a string when each key should be separated by delimiter. If a path item contains delimiter, it can be escaped
505
     * with "\" (backslash) or a custom delimiter can be used.
506
     * @param string $delimiter A separator, used to parse string key for embedded object property retrieving. Defaults
507
     * to "." (dot).
508
     * @param string $escapeCharacter An escape character, used to escape delimiter. Defaults to "\" (backslash).
509
     * @param bool $preserveDelimiterEscaping Whether to preserve delimiter escaping in the items of final array (in
510
     * case of using string as an input). When `false`, "\" (backslashes) are removed. For a "." as delimiter, "."
511
     * becomes "\.". Defaults to `false`.
512
     *
513
     * @return string[]
514
     */
515 34
    public static function parsePath(
516
        string $path,
517
        string $delimiter = '.',
518
        string $escapeCharacter = '\\',
519
        bool $preserveDelimiterEscaping = false
520
    ): array {
521 34
        if (strlen($delimiter) !== 1) {
522 1
            throw new InvalidArgumentException('Only 1 character is allowed for delimiter.');
523
        }
524
525 33
        if (strlen($escapeCharacter) !== 1) {
526 1
            throw new InvalidArgumentException('Only 1 escape character is allowed.');
527
        }
528
529 32
        if ($delimiter === $escapeCharacter) {
530 1
            throw new InvalidArgumentException('Delimiter and escape character must be different.');
531
        }
532
533 31
        if ($path === '') {
534 2
            return [];
535
        }
536
537 29
        $matches = preg_split(
538 29
            sprintf(
539
                '/(?<!%1$s)((?>%1$s%1$s)*)%2$s/',
540 29
                preg_quote($escapeCharacter, '/'),
541 29
                preg_quote($delimiter, '/')
542
            ),
543
            $path,
544
            -1,
545
            PREG_SPLIT_OFFSET_CAPTURE
546
        );
547 29
        $result = [];
548 29
        $countResults = count($matches);
549 29
        for ($i = 1; $i < $countResults; $i++) {
550 25
            $l = $matches[$i][1] - $matches[$i - 1][1] - strlen($matches[$i - 1][0]) - 1;
551 25
            $result[] = $matches[$i - 1][0] . ($l > 0 ? str_repeat($escapeCharacter, $l) : '');
552
        }
553 29
        $result[] = $matches[$countResults - 1][0];
554
555 29
        if ($preserveDelimiterEscaping === true) {
556 1
            return $result;
557
        }
558
559 28
        return array_map(
560 28
            static function (string $key) use ($delimiter, $escapeCharacter): string {
561 28
                return str_replace(
562
                    [
563 28
                        $escapeCharacter . $escapeCharacter,
564 28
                        $escapeCharacter . $delimiter,
565
                    ],
566
                    [
567 28
                        $escapeCharacter,
568
                        $delimiter,
569
                    ],
570
                    $key
571
                );
572
            },
573
            $result
574
        );
575
    }
576
}
577