Passed
Pull Request — master (#39)
by Alexander
01:11
created

StringHelper   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 480
Duplicated Lines 0 %

Test Coverage

Coverage 95.31%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 52
eloc 103
c 1
b 0
f 0
dl 0
loc 480
ccs 122
cts 128
cp 0.9531
rs 7.44

24 Methods

Rating   Name   Duplication   Size   Complexity  
A byteLength() 0 3 1
A startsWith() 0 8 2
B replaceSubstring() 0 21 8
A uppercaseFirstCharacterInEachWord() 0 9 1
A substring() 0 3 1
A sentence() 0 14 5
A directoryName() 0 8 2
A length() 0 3 1
A endsWith() 0 13 3
A explode() 0 21 5
A endsWithIgnoringCase() 0 8 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 8 2
A byteSubstring() 0 3 1
A countWords() 0 3 1
A truncateWords() 0 8 2
A lowercase() 0 3 1
A base64UrlDecode() 0 3 1
A truncateEnd() 0 10 2
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 mb_strlen;
9
use function mb_strtolower;
10
use function mb_strtoupper;
11
use function mb_substr;
12
13
/**
14
 * Provides static methods to work with strings.
15
 */
16
final class StringHelper
17
{
18
    /**
19
     * Returns the number of bytes in the given string.
20
     * This method ensures the string is treated as a byte array even if `mbstring.func_overload` is turned on
21
     * by using {@see mb_strlen()}.
22
     * @param string|null $input The string being measured for length.
23
     * @return int The number of bytes in the given string.
24
     */
25 40
    public static function byteLength(?string $input): int
26
    {
27 40
        return mb_strlen((string)$input, '8bit');
28
    }
29
30
    /**
31
     * Returns the portion of string specified by the start and length parameters.
32
     * This method ensures the string is treated as a byte array by using `mb_substr()`.
33
     * @param string $input The input string. Must be one character or longer.
34
     * @param int $start The starting position.
35
     * @param int|null $length The desired portion length. If not specified or `null`, there will be
36
     * no limit on length i.e. the output will be until the end of the string.
37
     * @return string The extracted part of string, or FALSE on failure or an empty string.
38
     * @see http://www.php.net/manual/en/function.substr.php
39
     */
40 1
    public static function byteSubstring(string $input, int $start, int $length = null): string
41
    {
42 1
        return mb_substr($input, $start, $length ?? mb_strlen($input, '8bit'), '8bit');
43
    }
44
45
    /**
46
     * Returns the trailing name component of a path.
47
     * This method is similar to the php function `basename()` except that it will
48
     * treat both \ and / as directory separators, independent of the operating system.
49
     * This method was mainly created to work on php namespaces. When working with real
50
     * file paths, PHP's `basename()` should work fine for you.
51
     * Note: this method is not aware of the actual filesystem, or path components such as "..".
52
     *
53
     * @param string $path A path string.
54
     * @param string $suffix If the name component ends in suffix this will also be cut off.
55
     * @return string The trailing name component of the given path.
56
     * @see http://www.php.net/manual/en/function.basename.php
57
     */
58 1
    public static function baseName(string $path, string $suffix = ''): string
59
    {
60 1
        $length = mb_strlen($suffix);
61 1
        if ($length > 0 && mb_substr($path, -$length) === $suffix) {
62 1
            $path = mb_substr($path, 0, -$length);
63
        }
64 1
        $path = rtrim(str_replace('\\', '/', $path), '/\\');
65 1
        $position = mb_strrpos($path, '/');
66 1
        if ($position !== false) {
67 1
            return mb_substr($path, $position + 1);
68
        }
69
70 1
        return $path;
71
    }
72
73
    /**
74
     * Returns parent directory's path.
75
     * This method is similar to `dirname()` except that it will treat
76
     * both \ and / as directory separators, independent of the operating system.
77
     *
78
     * @param string $path A path string.
79
     * @return string The parent directory's path.
80
     * @see http://www.php.net/manual/en/function.basename.php
81
     */
82 1
    public static function directoryName(string $path): string
83
    {
84 1
        $position = mb_strrpos(str_replace('\\', '/', $path), '/');
85 1
        if ($position !== false) {
86 1
            return mb_substr($path, 0, $position);
87
        }
88
89 1
        return '';
90
    }
91
92
    /**
93
     * Check if given string starts with specified substring.
94
     * Binary and multibyte safe.
95
     *
96
     * @param string $input Input string.
97
     * @param string|null $with Part to search inside the $string.
98
     * @return bool Returns true if first input starts with second input, false otherwise.
99
     */
100 19
    public static function startsWith(string $input, ?string $with): bool
101
    {
102 19
        $bytes = static::byteLength($with);
103 19
        if ($bytes === 0) {
104 3
            return true;
105
        }
106
107 16
        return strncmp($input, $with, $bytes) === 0;
108
    }
109
110
    /**
111
     * Check if given string starts with specified substring ignoring case.
112
     * Binary and multibyte safe.
113
     *
114
     * @param string $input Input string.
115
     * @param string|null $with Part to search inside the $string.
116
     * @return bool Returns true if first input starts with second input, false otherwise.
117
     */
118 1
    public static function startsWithIgnoringCase(string $input, ?string $with): bool
119
    {
120 1
        $bytes = static::byteLength($with);
121 1
        if ($bytes === 0) {
122 1
            return true;
123
        }
124
125 1
        return static::lowercase(static::substring($input, 0, $bytes, '8bit')) === static::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

125
        return static::lowercase(static::substring($input, 0, $bytes, '8bit')) === static::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
126
    }
127
128
    /**
129
     * Check if given string ends with specified substring.
130
     * Binary and multibyte safe.
131
     *
132
     * @param string $input Input string to check.
133
     * @param string|null $with Part to search inside of the $string.
134
     * @return bool Returns true if first input ends with second input, false otherwise.
135
     */
136 19
    public static function endsWith(string $input, ?string $with): bool
137
    {
138 19
        $bytes = static::byteLength($with);
139 19
        if ($bytes === 0) {
140 3
            return true;
141
        }
142
143
        // Warning check, see http://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues
144 16
        if (static::byteLength($input) < $bytes) {
145 3
            return false;
146
        }
147
148 13
        return substr_compare($input, $with, -$bytes, $bytes) === 0;
149
    }
150
151
    /**
152
     * Check if given string ends with specified substring.
153
     * Binary and multibyte safe.
154
     *
155
     * @param string $input Input string to check.
156
     * @param string|null $with Part to search inside of the $string.
157
     * @return bool Returns true if first input ends with second input, false otherwise.
158
     */
159 1
    public static function endsWithIgnoringCase(string $input, ?string $with): bool
160
    {
161 1
        $bytes = static::byteLength($with);
162 1
        if ($bytes === 0) {
163 1
            return true;
164
        }
165
166 1
        return static::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === static::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

166
        return static::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === static::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
167
    }
168
169
    /**
170
     * Truncates a string from the beginning to the number of characters specified.
171
     *
172
     * @param string $input String to process.
173
     * @param int $length Maximum length of the truncated string including trim marker.
174
     * @param string $trimMarker String to append to the beginning.
175
     * @param string $encoding The encoding to use, defaults to "UTF-8".
176
     * @return string
177
     */
178 1
    public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
179
    {
180 1
        $inputLength = mb_strlen($input, $encoding);
181
182 1
        if ($inputLength <= $length) {
183
            return $input;
184
        }
185
186 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
187 1
        return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
188
    }
189
190
    /**
191
     * Truncates a string in the middle. Keeping start and end.
192
     * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
193
     *
194
     * @param string $input The string to truncate.
195
     * @param int $length Maximum length of the truncated string including trim marker.
196
     * @param string $trimMarker String to append in the middle of truncated string.
197
     * @param string $encoding The encoding to use, defaults to "UTF-8".
198
     * @return string The truncated string.
199
     */
200 2
    public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
201
    {
202 2
        $inputLength = mb_strlen($input, $encoding);
203
204 2
        if ($inputLength <= $length) {
205 1
            return $input;
206
        }
207
208 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
209 1
        $start = (int)ceil(($length - $trimMarkerLength) / 2);
210 1
        $end = $length - $start - $trimMarkerLength;
211
212 1
        return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
213
    }
214
215
    /**
216
     * Truncates a string from the end to the number of characters specified.
217
     *
218
     * @param string $input The string to truncate.
219
     * @param int $length Maximum length of the truncated string including trim marker.
220
     * @param string $trimMarker String to append to the end of truncated string.
221
     * @param string $encoding The encoding to use, defaults to "UTF-8".
222
     * @return string The truncated string.
223
     */
224 1
    public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
225
    {
226 1
        $inputLength = mb_strlen($input, $encoding);
227
228 1
        if ($inputLength <= $length) {
229 1
            return $input;
230
        }
231
232 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
233 1
        return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
234
    }
235
236
    /**
237
     * Truncates a string to the number of words specified.
238
     *
239
     * @param string $input The string to truncate.
240
     * @param int $count How many words from original string to include into truncated string.
241
     * @param string $trimMarker String to append to the end of truncated string.
242
     * @return string The truncated string.
243
     */
244 1
    public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
245
    {
246 1
        $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
247 1
        if (count($words) / 2 > $count) {
0 ignored issues
show
Bug introduced by
It seems like $words can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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

247
        if (count(/** @scrutinizer ignore-type */ $words) / 2 > $count) {
Loading history...
248 1
            return implode('', array_slice($words, 0, ($count * 2) - 1)) . $trimMarker;
0 ignored issues
show
Bug introduced by
It seems like $words can also be of type false; however, parameter $array of array_slice() does only seem to accept array, 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

248
            return implode('', array_slice(/** @scrutinizer ignore-type */ $words, 0, ($count * 2) - 1)) . $trimMarker;
Loading history...
249
        }
250
251 1
        return $input;
252
    }
253
254
    /**
255
     * Get string length.
256
     *
257
     * @param string $string String to calculate length for.
258
     * @param string $encoding The encoding to use, defaults to "UTF-8".
259
     * @see https://php.net/manual/en/function.mb-strlen.php
260
     * @return int
261
     */
262 1
    public static function length(string $string, string $encoding = 'UTF-8'): int
263
    {
264 1
        return mb_strlen($string, $encoding);
265
    }
266
267
    /**
268
     * Counts words in a string.
269
     *
270
     * @param string $input
271
     * @return int
272
     */
273 1
    public static function countWords(string $input): int
274
    {
275 1
        return count(preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY));
0 ignored issues
show
Bug introduced by
It seems like preg_split('/\s+/u', $in...gs\PREG_SPLIT_NO_EMPTY) can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, 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

275
        return count(/** @scrutinizer ignore-type */ preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY));
Loading history...
276
    }
277
278
    /**
279
     * Make a string lowercase.
280
     *
281
     * @param string $string String to process.
282
     * @param string $encoding The encoding to use, defaults to "UTF-8".
283
     * @see https://php.net/manual/en/function.mb-strtolower.php
284
     * @return string
285
     */
286 3
    public static function lowercase(string $string, string $encoding = 'UTF-8'): string
287
    {
288 3
        return mb_strtolower($string, $encoding);
289
    }
290
291
    /**
292
     * Make a string uppercase.
293
     *
294
     * @param string $string String to process.
295
     * @param string $encoding The encoding to use, defaults to "UTF-8".
296
     * @see https://php.net/manual/en/function.mb-strtoupper.php
297
     * @return string
298
     */
299 15
    public static function uppercase(string $string, string $encoding = 'UTF-8'): string
300
    {
301 15
        return mb_strtoupper($string, $encoding);
302
    }
303
304
    /**
305
     * Make a string's first character uppercase.
306
     *
307
     * @param string $string The string to be processed.
308
     * @param string $encoding The encoding to use, defaults to "UTF-8".
309
     * @return string
310
     * @see https://php.net/manual/en/function.ucfirst.php
311
     */
312 14
    public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
313
    {
314 14
        $firstCharacter = static::substring($string, 0, 1, $encoding);
315 14
        $rest = static::substring($string, 1, null, $encoding);
316
317 14
        return static::uppercase($firstCharacter, $encoding) . $rest;
318
    }
319
320
    /**
321
     * Uppercase the first character of each word in a string.
322
     *
323
     * @param string $string The string to be processed.
324
     * @param string $encoding The encoding to use, defaults to "UTF-8".
325
     * @see https://php.net/manual/en/function.ucwords.php
326
     * @return string
327
     */
328 10
    public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
329
    {
330 10
        $words = preg_split("/\s/u", $string, -1, PREG_SPLIT_NO_EMPTY);
331
332 10
        $wordsWithUppercaseFirstCharacter = array_map(static function ($word) use ($encoding) {
333 9
            return static::uppercaseFirstCharacter($word, $encoding);
334 10
        }, $words);
0 ignored issues
show
Bug introduced by
It seems like $words can also be of type false; however, parameter $arr1 of array_map() does only seem to accept array, 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

334
        }, /** @scrutinizer ignore-type */ $words);
Loading history...
335
336 10
        return implode(' ', $wordsWithUppercaseFirstCharacter);
337
    }
338
339
    /**
340
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
341
     *
342
     * > Note: Base 64 padding `=` may be at the end of the returned string.
343
     * > `=` is not transparent to URL encoding.
344
     *
345
     * @see https://tools.ietf.org/html/rfc4648#page-7
346
     * @param string $input The string to encode.
347
     * @return string Encoded string.
348
     */
349 4
    public static function base64UrlEncode(string $input): string
350
    {
351 4
        return strtr(base64_encode($input), '+/', '-_');
352
    }
353
354
    /**
355
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
356
     *
357
     * @see https://tools.ietf.org/html/rfc4648#page-7
358
     * @param string $input Encoded string.
359
     * @return string Decoded string.
360
     */
361 4
    public static function base64UrlDecode(string $input): string
362
    {
363 4
        return base64_decode(strtr($input, '-_', '+/'));
364
    }
365
366
    /**
367
     * Explodes string into array, optionally trims values and skips empty ones.
368
     *
369
     * @param string $input String to be exploded.
370
     * @param string $delimiter Delimiter. Default is ','.
371
     * @param mixed $trim Whether to trim each element. Can be:
372
     *   - boolean - to trim normally;
373
     *   - string - custom characters to trim. Will be passed as a second argument to `trim()` function.
374
     *   - callable - will be called for each value instead of trim. Takes the only argument - value.
375
     * @param bool $skipEmpty Whether to skip empty strings between delimiters. Default is false.
376
     * @return array
377
     */
378 1
    public static function explode(string $input, string $delimiter = ',', $trim = true, bool $skipEmpty = false): array
379
    {
380 1
        $result = explode($delimiter, $input);
381 1
        if ($trim !== false) {
382 1
            if ($trim === true) {
383 1
                $trim = 'trim';
384 1
            } elseif (!\is_callable($trim)) {
385 1
                $trim = static function ($v) use ($trim) {
386 1
                    return trim($v, $trim);
387 1
                };
388
            }
389 1
            $result = array_map($trim, $result);
390
        }
391 1
        if ($skipEmpty) {
392
            // Wrapped with array_values to make array keys sequential after empty values removing
393 1
            $result = array_values(array_filter($result, static function ($value) {
394 1
                return $value !== '';
395 1
            }));
396
        }
397
398 1
        return $result;
399
    }
400
401
    /**
402
     * Get part of string.
403
     *
404
     * @param string $string To get substring from.
405
     * @param int $start Character to start at.
406
     * @param int|null $length Number of characters to get.
407
     * @param string $encoding The encoding to use, defaults to "UTF-8".
408
     * @see https://php.net/manual/en/function.mb-substr.php
409
     * @return string
410
     */
411 15
    public static function substring(string $string, int $start, int $length = null, string $encoding = 'UTF-8'): string
412
    {
413 15
        return mb_substr($string, $start, $length, $encoding);
414
    }
415
416
    /**
417
     * Replace text within a portion of a string.
418
     *
419
     * @param string $string The input string.
420
     * @param string $replacement The replacement string.
421
     * @param int $start Position to begin replacing substring at.
422
     * If start is non-negative, the replacing will begin at the start'th offset into string.
423
     * If start is negative, the replacing will begin at the start'th character from the end of string.
424
     * @param int|null $length Length of the substring to be replaced.
425
     * If given and is positive, it represents the length of the portion of string which is to be replaced.
426
     * If it is negative, it represents the number of characters from the end of string at which to stop replacing.
427
     * 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.
428
     * If length is zero then this function will have the effect of inserting replacement into string at the given start offset.
429
     * @param string $encoding The encoding to use, defaults to "UTF-8".
430
     * @return string
431
     */
432 2
    public static function replaceSubstring(string $string, string $replacement, int $start, ?int $length = null, string $encoding = 'UTF-8'): string
433
    {
434 2
        $stringLength = mb_strlen($string, $encoding);
435
436 2
        if ($start < 0) {
437
            $start = \max(0, $stringLength + $start);
438 2
        } elseif ($start > $stringLength) {
439
            $start = $stringLength;
440
        }
441
442 2
        if ($length !== null && $length < 0) {
443 2
            $length = \max(0, $stringLength - $start + $length);
444
        } elseif ($length === null || $length > $stringLength) {
445
            $length = $stringLength;
446
        }
447
448 2
        if (($start + $length) > $stringLength) {
449
            $length = $stringLength - $start;
450
        }
451
452 2
        return mb_substr($string, 0, $start, $encoding) . $replacement . mb_substr($string, $start + $length, $stringLength - $start - $length, $encoding);
453
    }
454
455
    /**
456
     * Converts a list of words into a sentence.
457
     *
458
     * Special treatment is done for the last few words. For example,
459
     *
460
     * ```php
461
     * $words = ['Spain', 'France'];
462
     * echo Inflector::sentence($words);
463
     * // output: Spain and France
464
     *
465
     * $words = ['Spain', 'France', 'Italy'];
466
     * echo Inflector::sentence($words);
467
     * // output: Spain, France and Italy
468
     *
469
     * $words = ['Spain', 'France', 'Italy'];
470
     * echo Inflector::sentence($words, ' & ');
471
     * // output: Spain, France & Italy
472
     * ```
473
     *
474
     * @param array $words The words to be converted into an string.
475
     * @param string $twoWordsConnector The string connecting words when there are only two. Default to " and ".
476
     * @param string|null $lastWordConnector The string connecting the last two words. If this is null, it will
477
     * take the value of `$twoWordsConnector`.
478
     * @param string $connector The string connecting words other than those connected by
479
     * $lastWordConnector and $twoWordsConnector.
480
     * @return string The generated sentence.
481
     */
482 1
    public static function sentence(array $words, string $twoWordsConnector = ' and ', ?string $lastWordConnector = null, string $connector = ', '): ?string
483
    {
484 1
        if ($lastWordConnector === null) {
485 1
            $lastWordConnector = $twoWordsConnector;
486
        }
487 1
        switch (count($words)) {
488 1
            case 0:
489 1
                return '';
490 1
            case 1:
491 1
                return reset($words);
492 1
            case 2:
493 1
                return implode($twoWordsConnector, $words);
494
            default:
495 1
                return implode($connector, \array_slice($words, 0, -1)) . $lastWordConnector . end($words);
496
        }
497
    }
498
}
499