Passed
Push — master ( de1015...94b82d )
by Alexander
02:12 queued 12s
created

StringHelper   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 477
Duplicated Lines 0 %

Test Coverage

Coverage 91.23%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 91
dl 0
loc 477
ccs 104
cts 114
cp 0.9123
rs 8.64
c 2
b 0
f 0
wmc 47

23 Methods

Rating   Name   Duplication   Size   Complexity  
A startsWith() 0 16 4
B replaceSubstring() 0 21 8
A uppercaseFirstCharacterInEachWord() 0 9 1
A byteLength() 0 3 1
A substring() 0 3 1
A length() 0 3 1
A directoryName() 0 8 2
A endsWith() 0 21 5
A split() 0 4 1
A endsWithIgnoringCase() 0 11 2
A truncateMiddle() 0 13 2
A truncateBegin() 0 10 2
A baseName() 0 13 4
A uppercaseFirstCharacter() 0 6 1
A uppercase() 0 3 1
A startsWithIgnoringCase() 0 11 2
A byteSubstring() 0 3 1
A countWords() 0 3 1
A truncateWords() 0 10 2
A truncateEnd() 0 10 2
A lowercase() 0 3 1
A base64UrlDecode() 0 3 1
A base64UrlEncode() 0 3 1

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

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

263
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
264
    }
265
266
    /**
267
     * Truncates a string from the beginning to the number of characters specified.
268
     *
269
     * @param string $input String to process.
270
     * @param int $length Maximum length of the truncated string including trim marker.
271
     * @param string $trimMarker String to append to the beginning.
272
     * @param string $encoding The encoding to use, defaults to "UTF-8".
273
     *
274
     * @return string
275
     */
276 1
    public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
277
    {
278 1
        $inputLength = mb_strlen($input, $encoding);
279
280 1
        if ($inputLength <= $length) {
281 1
            return $input;
282
        }
283
284 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
285 1
        return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
286
    }
287
288
    /**
289
     * Truncates a string in the middle. Keeping start and end.
290
     * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
291
     *
292
     * @param string $input The string to truncate.
293
     * @param int $length Maximum length of the truncated string including trim marker.
294
     * @param string $trimMarker String to append in the middle of truncated string.
295
     * @param string $encoding The encoding to use, defaults to "UTF-8".
296
     *
297
     * @return string The truncated string.
298
     */
299 2
    public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
300
    {
301 2
        $inputLength = mb_strlen($input, $encoding);
302
303 2
        if ($inputLength <= $length) {
304 1
            return $input;
305
        }
306
307 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
308 1
        $start = (int)ceil(($length - $trimMarkerLength) / 2);
309 1
        $end = $length - $start - $trimMarkerLength;
310
311 1
        return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
312
    }
313
314
    /**
315
     * Truncates a string from the end to the number of characters specified.
316
     *
317
     * @param string $input The string to truncate.
318
     * @param int $length Maximum length of the truncated string including trim marker.
319
     * @param string $trimMarker String to append to the end of truncated string.
320
     * @param string $encoding The encoding to use, defaults to "UTF-8".
321
     *
322
     * @return string The truncated string.
323
     */
324 1
    public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
325
    {
326 1
        $inputLength = mb_strlen($input, $encoding);
327
328 1
        if ($inputLength <= $length) {
329 1
            return $input;
330
        }
331
332 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
333 1
        return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
334
    }
335
336
    /**
337
     * Truncates a string to the number of words specified.
338
     *
339
     * @param string $input The string to truncate.
340
     * @param int $count How many words from original string to include into truncated string.
341
     * @param string $trimMarker String to append to the end of truncated string.
342
     *
343
     * @return string The truncated string.
344
     */
345 1
    public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
346
    {
347 1
        $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
348 1
        if (count($words) / 2 > $count) {
349
            /** @var string[] $words */
350 1
            $words = array_slice($words, 0, ($count * 2) - 1);
351 1
            return implode('', $words) . $trimMarker;
352
        }
353
354 1
        return $input;
355
    }
356
357
    /**
358
     * Get string length.
359
     *
360
     * @param string $string String to calculate length for.
361
     * @param string $encoding The encoding to use, defaults to "UTF-8".
362
     *
363
     * @see https://php.net/manual/en/function.mb-strlen.php
364
     *
365
     * @return int
366
     */
367 1
    public static function length(string $string, string $encoding = 'UTF-8'): int
368
    {
369 1
        return mb_strlen($string, $encoding);
370
    }
371
372
    /**
373
     * Counts words in a string.
374
     *
375
     * @param string $input
376
     *
377
     * @return int
378
     */
379 1
    public static function countWords(string $input): int
380
    {
381 1
        return count(preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY));
382
    }
383
384
    /**
385
     * Make a string lowercase.
386
     *
387
     * @param string $string String to process.
388
     * @param string $encoding The encoding to use, defaults to "UTF-8".
389
     *
390
     * @see https://php.net/manual/en/function.mb-strtolower.php
391
     *
392
     * @return string
393
     */
394 3
    public static function lowercase(string $string, string $encoding = 'UTF-8'): string
395
    {
396 3
        return mb_strtolower($string, $encoding);
397
    }
398
399
    /**
400
     * Make a string uppercase.
401
     *
402
     * @param string $string String to process.
403
     * @param string $encoding The encoding to use, defaults to "UTF-8".
404
     *
405
     * @see https://php.net/manual/en/function.mb-strtoupper.php
406
     *
407
     * @return string
408
     */
409 15
    public static function uppercase(string $string, string $encoding = 'UTF-8'): string
410
    {
411 15
        return mb_strtoupper($string, $encoding);
412
    }
413
414
    /**
415
     * Make a string's first character uppercase.
416
     *
417
     * @param string $string The string to be processed.
418
     * @param string $encoding The encoding to use, defaults to "UTF-8".
419
     *
420
     * @return string
421
     *
422
     * @see https://php.net/manual/en/function.ucfirst.php
423
     */
424 14
    public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
425
    {
426 14
        $firstCharacter = self::substring($string, 0, 1, $encoding);
427 14
        $rest = self::substring($string, 1, null, $encoding);
428
429 14
        return self::uppercase($firstCharacter, $encoding) . $rest;
430
    }
431
432
    /**
433
     * Uppercase the first character of each word in a string.
434
     *
435
     * @param string $string The string to be processed.
436
     * @param string $encoding The encoding to use, defaults to "UTF-8".
437
     *
438
     * @see https://php.net/manual/en/function.ucwords.php
439
     *
440
     * @return string
441
     */
442 10
    public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
443
    {
444 10
        $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY);
445
446 10
        $wordsWithUppercaseFirstCharacter = array_map(static function (string $word) use ($encoding) {
447 9
            return self::uppercaseFirstCharacter($word, $encoding);
448 10
        }, $words);
449
450 10
        return implode(' ', $wordsWithUppercaseFirstCharacter);
451
    }
452
453
    /**
454
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
455
     *
456
     * > Note: Base 64 padding `=` may be at the end of the returned string.
457
     * > `=` is not transparent to URL encoding.
458
     *
459
     * @see https://tools.ietf.org/html/rfc4648#page-7
460
     *
461
     * @param string $input The string to encode.
462
     *
463
     * @return string Encoded string.
464
     */
465 4
    public static function base64UrlEncode(string $input): string
466
    {
467 4
        return strtr(base64_encode($input), '+/', '-_');
468
    }
469
470
    /**
471
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
472
     *
473
     * @see https://tools.ietf.org/html/rfc4648#page-7
474
     *
475
     * @param string $input Encoded string.
476
     *
477
     * @return string Decoded string.
478
     */
479 4
    public static function base64UrlDecode(string $input): string
480
    {
481 4
        return base64_decode(strtr($input, '-_', '+/'));
482
    }
483
484
    /**
485
     * Split a string to array with non-empty lines.
486
     * Whitespace from the beginning and end of a each line will be stripped.
487
     *
488
     * @param string $string The input string.
489
     * @param string $separator The boundary string. It is a part of regular expression
490
     * so should be taken into account or properly escaped with {@see preg_quote()}.
491
     *
492
     * @return array
493
     */
494 15
    public static function split(string $string, string $separator = '\R'): array
495
    {
496 15
        $string = preg_replace('(^\s*|\s*$)', '', $string);
497 15
        return preg_split('~\s*' . $separator . '\s*~', $string, -1, PREG_SPLIT_NO_EMPTY);
498
    }
499
}
500