Issues (94)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php namespace nyx\utils;
2
3
/**
4
 * Str
5
 *
6
 * Helper methods for dealing with strings. The class is based on Laravel, FuelPHP, Patchwork/UTF-8 and a few others.
7
 * Some minor performance-related improvements were made.
8
 *
9
 * Note: Many of the methods can be circumvented by falling back directly to the builtin functions of the mbstring
10
 * extension as long as you don't need the additional layer of abstraction and feel comfortable managing the
11
 * encoding and input parameter validity on your own.
12
 *
13
 * Suggestions:
14
 *   If you need an instance-based fluent OO wrapper for strings with similar manipulation capabilities. then
15
 *   you should take a look at Stringy {@see https://github.com/danielstjules/Stringy}
16
 *
17
 * Requires:
18
 * - ext-mbstring
19
 * - ext-intl (Normalizer)
20
 * - ext-iconv
21
 *
22
 * @package     Nyx\Utils\Strings
23
 * @version     0.1.0
24
 * @author      Michal Chojnacki <[email protected]>
25
 * @copyright   2012-2016 Nyx Dev Team
26
 * @link        http://docs.muyo.io/nyx/utils/strings.html
27
 * @todo        Add afterFirst/Last, beforeFirst/Last instead of the current after/before?
28
 */
29
class Str
30
{
31
    /**
32
     * The traits of the Str class.
33
     */
34
    use traits\StaticallyExtendable;
35
36
    /**
37
     * @var string  The default encoding to use when we fail to determine it based on a given string. UTF-8 will
38
     *              be used when the above is true and this property is null.
39
     */
40
    public static $encoding;
41
42
    /**
43
     * @var bool    Whether self::encoding(), used internally by all methods which accept an encoding,
44
     *              should attempt to determine the encoding from the string given to it, if it's not
45
     *              explicitly specified. Note: This adds (relatively big) overhead to most methods and
46
     *              is therefore set to false, since the default of UTF-8 should satisfy most use-cases.
47
     */
48
    public static $autoDetectEncoding = false;
49
50
    /**
51
     * @var array   The transliteration table used by the toAscii() method once fetched from a file.
52
     */
53
    protected static $ascii;
54
55
    /**
56
     * Finds the first occurrence of $needle within $haystack and returns the part of $haystack
57
     * after the $needle (excluding the $needle).
58
     *
59
     * @param   string      $haystack   The string to search in.
60
     * @param   string      $needle     The substring to search for.
61
     * @param   bool        $strict     Whether to use case-sensitive comparisons. True by default.
62
     * @param   string|null $encoding   The encoding to use.
63
     * @return  string                  The part of $haystack after $needle.
64
     * @throws  \RuntimeException       Upon failing to find $needle in $haystack at all.
65
     */
66
    public static function after(string $haystack, string $needle, bool $strict = true, string $encoding = null) : string
67
    {
68
        $encoding = $encoding ?: static::encoding($haystack);
69
70
        // We're gonna use strstr internally to grab the part of $haystack starting at $needle
71
        // and including the $needle, and then simply remove the starting $needle.
72
        // Note: Removing 1 from the length of $needle since we're using it as start offset
73
        // for mb_substr which is 0-indexed.
74
        return mb_substr(static::partOf($haystack, $needle, $strict, false, $encoding), mb_strlen($needle, $encoding) - 1);
75
    }
76
77
    /**
78
     * Finds the first occurrence of $needle within $haystack and returns the part of $haystack
79
     * *before* the $needle.
80
     *
81
     * @param   string      $haystack   The string to search in.
82
     * @param   string      $needle     The substring to search for.
83
     * @param   bool        $strict     Whether to use case-sensitive comparisons. True by default.
84
     * @param   string|null $encoding   The encoding to use.
85
     * @return  string                  The part of $haystack before $needle.
86
     * @throws  \RuntimeException       Upon failing to find $needle in $haystack at all.
87
     */
88
    public static function before(string $haystack, string $needle, bool $strict = true, string $encoding = null) : string
89
    {
90
        return static::partOf($haystack, $needle, $strict, true, $encoding);
91
    }
92
93
    /**
94
     * Ensures the given $haystack begins with a single instance of the given $needle. For single characters
95
     * use PHP's native ltrim() instead.
96
     *
97
     * @param   string  $haystack   The string to cap.
98
     * @param   string  $needle     The substring to begin with.
99
     * @return  string              The resulting string.
100
     */
101
    public static function beginWith(string $haystack, string $needle) : string
102
    {
103
        return $needle . preg_replace('/^(?:'.preg_quote($needle, '/').')+/', '', $haystack);
104
    }
105
106
    /**
107
     * Returns the substring of $haystack between $startNeedle and $endNeedle.
108
     *
109
     * If all you need is to return part of a string when the offsets you are looking for are known,
110
     * you should use {@see self::sub()} instead.
111
     *
112
     * @param   string      $haystack       The string to search in.
113
     * @param   string      $startNeedle    The needle marking the start of the substring to return.
114
     * @param   string      $endNeedle      The needle marking the end of the substring to return.
115
     * @param   int         $offset         The 0-based index at which to start the search for the first needle.
116
     *                                      Can be negative, in which case the search will start $offset characters
117
     *                                      from the end of the $haystack.
118
     * @param   bool        $strict         Whether to use strict comparisons when searching for the needles.
119
     * @param   string|null $encoding       The encoding to use.
120
     * @return  string                      The substring between the needles.
121
     * @throws  \InvalidArgumentException   When $haystack or either of the needles is an empty string.
122
     * @throws  \OutOfBoundsException       Upon failing to find either $startNeedle or $endNeedle in $haystack.
123
     */
124
    public static function between(string $haystack, string $startNeedle, string $endNeedle, int $offset = 0, bool $strict = true, string $encoding = null) : string
125
    {
126
        // Note: We're throwing here because this method will return unpredictable results otherwise.
127
        // If you want to omit $firstNeedle or $secondNeedle, turn to self::after(), self::before()
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
128
        // and self::from() instead.
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
129
        // We're not validating input args any further than the below, however, as there are a lot of
130
        // error-inducing combinations of invalid input which all will be caught when we attempt
131
        // to actually look for the indices of the needles. Anything else is just way too much overhead.
132
        if ($haystack === '' || $startNeedle === '' || $endNeedle === '') {
133
            $endNeedle   === '' && $arg = '$endNeedle';
134
            $startNeedle === '' && $arg = '$startNeedle';
135
            $haystack    === '' && $arg = '$haystack';
136
137
            throw new \InvalidArgumentException($arg.' must not be an empty string.');
138
        }
139
140
        $encoding    = $encoding ?: static::encoding($haystack);
141
        $funcIndexOf = $strict ? 'mb_strpos' : 'mb_stripos';
142
143
        // Find the offset of the first needle.
144 View Code Duplication
        if (false === $firstIndex = $funcIndexOf($haystack, $startNeedle, $offset, $encoding)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
145
            throw new \OutOfBoundsException('Failed to find $startNeedle ['.$startNeedle.'] in $haystack ['.static::truncate($haystack, 20, '...', false, $encoding).'].');
146
        }
147
148
        // We're going to adjust the offset for the position of the first needle, ie. we're gonna
149
        // start searching for the second one right at the end of the first one.
150
        $offset = $firstIndex + mb_strlen($startNeedle, $encoding);
151
152
        // Find the offset of the second needle.
153 View Code Duplication
        if (false === $secondIndex = $funcIndexOf($haystack, $endNeedle, $offset, $encoding)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
154
            throw new \OutOfBoundsException('Failed to find $endNeedle ['.$endNeedle.'] in $haystack ['.static::truncate($haystack, 20, '...', false, $encoding).'].');
155
        }
156
157
        // Return the substring between the needles.
158
        return mb_substr($haystack, $offset, $secondIndex - $offset);
159
    }
160
161
    /**
162
     * Returns the characters of the given $haystack as an array, in an offset => character format.
163
     *
164
     * @param   string      $haystack   The string to iterate over.
165
     * @param   string|null $encoding   The encoding to use.
166
     * @return  array                   The array of characters.
167
     */
168
    public static function characters(string $haystack, string $encoding = null) : array
169
    {
170
        $result = [];
171
        $length = mb_strlen($haystack, $encoding ?: static::encoding($haystack));
172
173 View Code Duplication
        for ($idx = 0; $idx < $length; $idx++) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
            $result[] = mb_substr($haystack, $idx, 1, $encoding);
175
        }
176
177
        return $result;
178
    }
179
180
    /**
181
     * Trims the given string and replaces multiple consecutive whitespaces with a single whitespace.
182
     *
183
     * Note: This includes multi-byte whitespace characters, tabs and newlines, which effectively means
184
     * that the string may also be collapsed down. This is mostly a utility for processing natural language
185
     * user input for displaying.
186
     *
187
     * @param   string          $str        The string to collapse.
188
     * @param   string|null     $encoding   The encoding to use.
189
     * @return  string                      The resulting string.
190
     */
191
    public static function collapse(string $str, string $encoding = null) : string
192
    {
193
        $encoding = $encoding ?: static::encoding($str);
194
195
        return static::trim(static::replace($str, '[[:space:]]+', ' ', 'msr', $encoding), null, $encoding);
196
    }
197
198
    /**
199
     * Determines whether the given $haystack contains the given $needle.
200
     *
201
     * If $needle is an array, this method will check whether at least one of the substrings is contained
202
     * in the $haystack. If $all is set to true, all needles in the $needle array must be contained in the
203
     * $haystack for this method to return true.
204
     *
205
     * @param   string          $haystack   The string to check in.
206
     * @param   string|array    $needle     A string or an array of strings. If an array is given, the method returns
207
     *                                      true if at least one of the values is contained within the $haystack.
208
     * @param   bool            $all        Set this to true to ensure all elements of the $needle array (if provided)
209
     *                                      are contained within the $haystack.
210
     * @param   bool            $strict     Whether to use case-sensitive comparisons.
211
     * @param   string|null     $encoding   The encoding to use.
212
     * @return  bool
213
     */
214
    public static function contains(string $haystack, $needle, bool $all = false, bool $strict = true, string $encoding = null) : bool
215
    {
216
        $func     = $strict ? 'mb_strpos' : 'mb_stripos';
217
        $encoding = $encoding ?: static::encoding($haystack);
218
219
        foreach ((array) $needle as $value) {
220
            if ($func($haystack, $value, 0, $encoding) === false) {
221
                if ($all) {
222
                    return false;
223
                }
224
            } elseif (!$all) {
225
                return true;
226
            }
227
        }
228
229
        // When we reach this point, if we were ensuring all needles are contained within the haystack, it means
230
        // that we didn't fail on a single one. However, when looking for at least one of them, it means that none
231
        // returned true up to this point, so none of them is contained in the haystack.
232
        return $all;
233
    }
234
235
    /**
236
     * Determines whether the given $haystack contains all of the $needles. Alias for self::contains() with an
237
     * array of needles and the $all parameter set to true.
238
     *
239
     * @see Str::contains()
240
     */
241
    public static function containsAll(string $haystack, array $needles, bool $strict = true, string $encoding = null) : bool
242
    {
243
        return static::contains($haystack, $needles, true, $strict, $encoding);
244
    }
245
246
    /**
247
     * Determines whether the given $haystack contains any of the $needles. Alias for self::contains() with an
248
     * array of needles and the $all parameter set to false.
249
     *
250
     * @see Str::contains()
251
     */
252
    public static function containsAny(string $haystack, array $needles, bool $strict = true, string $encoding = null) : bool
253
    {
254
        return static::contains($haystack, $needles, false, $strict, $encoding);
255
    }
256
257
    /**
258
     * Runs the given callable over each line of the given string and returns the resulting string.
259
     *
260
     * The callable should accept two arguments (in this order): the contents of the line (string)
261
     * and the line's number (int). Additional arguments may also be added and will be appended to the
262
     * callable in the order given. The callable should return a string or a value castable to a string.
263
     *
264
     * @param   string      $str        The string over which to run the callable.
265
     * @param   callable    $callable   The callable to apply.
266
     * @param   mixed       ...$args    Additional arguments to pass to the callable.
267
     * @return  string                  The string after applying the callable to each of its lines.
268
     */
269
    public static function eachLine(string $str, callable $callable, ...$args) : string
270
    {
271
        if ($str === '') {
272
            return $str;
273
        }
274
275
        $lines = mb_split('[\r\n]{1,2}', $str);
276
277
        foreach ($lines as $number => &$line) {
278
            $lines[$number] = (string) call_user_func($callable, $line, $number, ...$args);
279
        }
280
281
        return implode("\n", $lines);
282
    }
283
284
    /**
285
     * Attempts to determine the encoding of a string if a string is given.
286
     *
287
     * Upon failure or when no string is given, returns the static encoding set in this class or if that is not set,
288
     * the hardcoded default of 'utf-8'.
289
     *
290
     * @param   string|null $str
291
     * @return  string
292
     */
293
    public static function encoding(string $str = null) : string
294
    {
295
        // If a string was given, we attempt to detect the encoding of the string if we are told to do so.
296
        // If we succeed, just return the determined type.
297
        if (true === static::$autoDetectEncoding && null !== $str && false !== $encoding = mb_detect_encoding($str)) {
298
            return $encoding;
299
        }
300
301
        // Otherwise let's return one of the defaults.
302
        return static::$encoding ?: 'utf-8';
303
    }
304
305
    /**
306
     * Determines whether the given $haystack ends with the given needle or one of the given needles
307
     * if $needles is an array.
308
     *
309
     * @param   string          $haystack   The string to search in.
310
     * @param   string|array    $needles    The needle(s) to look for.
311
     * @param   bool            $strict     Whether to use case-sensitive comparisons.
312
     * @param   string|null     $encoding   The encoding to use.
313
     * @return  bool                        True when the string ends with one of the given needles, false otherwise.
314
     */
315
    public static function endsWith(string $haystack, $needles, bool $strict = true, string $encoding = null) : bool
316
    {
317
        if ($haystack === '') {
318
            return false;
319
        }
320
321
        $encoding = $encoding ?: static::encoding($haystack);
322
323
        foreach ((array) $needles as $needle) {
324
325
            // Empty needles are invalid. For philosophical reasons.
326
            if ($needle === '') {
327
                continue;
328
            }
329
330
            // Grab the substring of the haystack at an offset $needle would be at if the haystack
331
            // ended with it.
332
            $end = mb_substr($haystack, -mb_strlen($needle, $encoding), null, $encoding);
333
334
            // For case-insensitive comparisons we need a common denominator.
335
            if (!$strict) {
336
                $needle = mb_strtolower($needle, $encoding);
337
                $end    = mb_strtolower($end, $encoding);
338
            }
339
340
            // Stop looping on the first hit. Obviously we're not checking whether the $haystack
341
            // ends with *all* $needles, d'oh.
342
            if ($needle === $end) {
343
                return true;
344
            }
345
        }
346
347
        return false;
348
    }
349
350
    /**
351
     * Ensures the given $haystack ends with a single instance of the given $needle. For single characters
352
     * use PHP's native rtrim() instead.
353
     *
354
     * @param   string  $haystack   The string to cap.
355
     * @param   string  $needle     The substring to end with.
356
     * @return  string              The resulting string.
357
     */
358
    public static function finishWith(string $haystack, string $needle) : string
359
    {
360
        return preg_replace('/(?:'.preg_quote($needle, '/').')+$/', '', $haystack) . $needle;
361
    }
362
363
    /**
364
     * Finds the first occurrence of $needle within $haystack and returns the part of $haystack
365
     * starting where the $needle starts (ie. including the $needle).
366
     *
367
     * @param   string      $haystack   The string to search in.
368
     * @param   string      $needle     The substring to search for.
369
     * @param   bool        $strict     Whether to use case-sensitive comparisons. True by default.
370
     * @param   string|null $encoding   The encoding to use.
371
     * @return  string                  The part of $haystack from where $needle starts, including $needle.
372
     * @throws  \RuntimeException       Upon failing to find $needle in $haystack at all.
373
     */
374
    public static function from(string $haystack, string $needle, bool $strict = true, string $encoding = null) : string
375
    {
376
        return static::partOf($haystack, $needle, $strict, false, $encoding);
377
    }
378
379
    /**
380
     * Returns the index (0-indexed) of the first occurrence of $needle in the $haystack.
381
     *
382
     * Important note: Differs from native PHP strpos() in that if the $needle could not be found, it returns -1
383
     * instead of false.
384
     *
385
     * @param   string      $haystack   The string to search in.
386
     * @param   string      $needle     The substring to search for.
387
     * @param   int         $offset     The offset from which to search. Negative offsets will start the search $offset
388
     *                                  characters from the end of the $haystack. 0-indexed.
389
     * @param   bool        $strict     Whether to use case-sensitive comparisons. True by default.
390
     * @param   string|null $encoding   The encoding to use.
391
     * @return  int                     The index of the first occurrence if found, -1 otherwise.
392
     */
393 View Code Duplication
    public static function indexOf(string $haystack, string $needle, int $offset = 0, bool $strict = true, string $encoding = null) : int
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394
    {
395
        $func     = $strict ? 'mb_strpos' : 'mb_stripos';
396
        $encoding = $encoding ?: static::encoding($haystack);
397
398
        if (false === $result = $func($haystack, $needle, $offset, $encoding)) {
399
            return -1;
400
        }
401
402
        return $result;
403
    }
404
405
    /**
406
     * Returns the index (0-indexed) of the last occurrence of $needle in the $haystack.
407
     *
408
     * Important note: Differs from native PHP strrpos() in that if the $needle could not be found, it returns -1
409
     * instead of false.
410
     *
411
     * @param   string  $haystack   The string to search in.
412
     * @param   string  $needle     The substring to search for.
413
     * @param   int     $offset     The offset from which to search. Negative offsets will start the search $offset
414
     *                              characters from the end of the $haystack. 0-indexed.
415
     * @param   bool    $strict     Whether to use case-sensitive comparisons. True by default.
416
     * @param   string  $encoding   The encoding to use.
417
     * @return  int                 The index of the last occurrence if found, -1 otherwise.
418
     */
419 View Code Duplication
    public static function indexOfLast(string $haystack, string $needle, int $offset = 0, bool $strict = true, string $encoding = null) : int
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
420
    {
421
        $func     = $strict ? 'mb_strrpos' : 'mb_strripos';
422
        $encoding = $encoding ?: static::encoding($haystack);
423
424
        if (false === $result = $func($haystack, $needle, $offset, $encoding)) {
425
            return -1;
426
        }
427
428
        return $result;
429
    }
430
431
    /**
432
     * Inserts the given $needle into the $haystack at the provided offset.
433
     *
434
     * @param   string      $haystack       The string to insert into.
435
     * @param   string      $needle         The string to be inserted.
436
     * @param   int         $offset         The offset at which to insert the substring. If negative, the $substring
437
     *                                      will be inserted $offset characters from the end of $str.
438
     * @param   string|null $encoding       The encoding to use.
439
     * @return  string                      The resulting string.
440
     * @throws  \OutOfBoundsException       When trying to insert a substring at an index above the length of the
441
     *                                      initial string.
442
     */
443
    public static function insert(string $haystack, string $needle, int $offset, string $encoding = null) : string
444
    {
445
        if ($needle === '') {
446
            return $haystack;
447
        }
448
449
        $encoding = $encoding ?: static::encoding($haystack);
450
        $length   = mb_strlen($haystack, $encoding);
451
452
        // Make sure the offset is contained in the initial string.
453 View Code Duplication
        if (abs($offset) >= $length) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
454
            throw new \OutOfBoundsException('The given $offset ['.$offset.'] does not exist within the string ['.static::truncate($haystack, 20, '...', false, $encoding).'].');
455
        }
456
457
        // With a negative offset, we'll convert it to a positive one for the initial part (before the inserted
458
        // substring), since we'll be using that as a length actually.
459
        return mb_substr($haystack, 0, $offset < 0 ? $length + $offset : $offset, $encoding) . $needle . mb_substr($haystack, $offset, null, $encoding);
460
    }
461
462
    /**
463
     * Determines the length of a given string. Counts multi-byte characters as single characters.
464
     *
465
     * @param   string  $str        The string to count characters in.
466
     * @param   string  $encoding   The encoding to use.
467
     * @return  int                 The length of the string.
468
     */
469
    public static function length(string $str, string $encoding = null) : int
470
    {
471
        return mb_strlen($str, $encoding ?: static::encoding($str));
472
    }
473
474
    /**
475
     * Returns all lines contained in the given string as an array, ie. splits the string on newline characters
476
     * into separate strings as items in an array.
477
     *
478
     * @param   string  $str    The string to split into separate lines.
479
     * @param   int     $limit  The maximal number of lines to return. If null, all lines will be returned.
480
     * @return  array           An array of all lines contained in $str. An empty array when $str contains
481
     *                          no lines.
482
     */
483
    public static function lines(string $str, int $limit = null) : array
484
    {
485
        return mb_split('[\r\n]{1,2}', $str, $limit);
486
    }
487
488
    /**
489
     * Determines whether the given string matches the given pattern. Asterisks are translated into zero or more
490
     * regexp wildcards, allowing for glob-style patterns.
491
     *
492
     * @param   string  $str        The string to match.
493
     * @param   string  $pattern    The pattern to match the string against.
494
     * @return  bool
495
     */
496
    public static function matches(string $str, string $pattern) : bool
497
    {
498
        if ($pattern === $str) {
499
            return true;
500
        }
501
502
        return (bool) preg_match('#^'.str_replace('\*', '.*', preg_quote($pattern, '#')).'\z'.'#', $str);
503
    }
504
505
    /**
506
     * Returns an array containing the offsets of all occurrences of $needle in $haystack. The offsets
507
     * are 0-indexed. If no occurrences could be found, an empty array will be returned.
508
     *
509
     * If all you need is to count the number of occurrences, mb_substr_count() should be your
510
     * function of choice. Str::occurrences() however has got you covered if you need case-(in)sensitivity
511
     * or a count from a specific offset. To count the number of occurrences of $needle in $haystack using
512
     * this method, a simple `count(Str::occurrences($needle, $haystack));` will do the trick.
513
     *
514
     * @param   string      $haystack       The string to search in.
515
     * @param   string      $needle         The substring to search for.
516
     * @param   int         $offset         The offset from which to start the search. Can be negative, in which
517
     *                                      case this method will start searching for the occurrences $offset
518
     *                                      characters from the end of the $haystack. Starts from 0 by default.
519
     * @param   bool        $strict         Whether to use case-sensitive comparisons. True by default.
520
     * @param   string|null $encoding       The encoding to use.
521
     * @return  array                       An array containing the 0-indexed offsets of all found occurrences
522
     *                                      or an empty array if none were found.
523
     * @throws  \OutOfBoundsException       When the $offset index is not contained in the input string.
524
     */
525
    public static function occurrences(string $haystack, string $needle, int $offset = 0, bool $strict = true, string $encoding = null) : array
526
    {
527
        // Early return in obvious circumstances.
528
        if ($haystack === '' || $needle === '') {
529
            return [];
530
        }
531
532
        $encoding = $encoding ?: static::encoding($haystack);
533
        $length   = mb_strlen($haystack, $encoding);
534
535
        // Make sure the offset given exists within the $haystack.
536 View Code Duplication
        if (abs($offset) >= $length) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
537
            throw new \OutOfBoundsException('The given $offset ['.$offset.'] does not exist within the string ['.static::truncate($haystack, 20, '...', false, $encoding).'].');
538
        }
539
540
        $func   = $strict ? 'mb_strpos' : 'mb_stripos';
541
        $result = [];
542
543
        while (false !== $offset = $func($haystack, $needle, $offset, $encoding)) {
544
            // We could count the length of $needle here but just going +1 ensures we don't catch
545
            // the same needle again while at the same time we're avoiding the overhead of mb_strlen.
546
            $result[] = $offset++;
547
        }
548
549
        return $result;
550
    }
551
552
    /**
553
     * Pads the given string to a given $length using $with as padding. This has no effect on input strings which are
554
     * longer than or equal in length to the requested $length.
555
     *
556
     * Padding can be applied to the left, the right or both sides of the input string simultaneously depending
557
     * on the $type chosen (one of PHP's native STR_PAD_* constants).
558
     *
559
     * @param   string      $str                The string to pad.
560
     * @param   int         $length             The desired length of the string after padding.
561
     * @param   string      $with               The character(s) to pad the string with.
562
     * @param   int         $type               One of the STR_PAD_* constants supported by PHP's native str_pad().
563
     * @param   string|null $encoding           The encoding to use.
564
     * @return  string                          The resulting string.
565
     * @throws  \InvalidArgumentException       when an unrecognized $type is given.
566
     */
567
    public static function pad(string $str, int $length, string $with = ' ', int $type = STR_PAD_RIGHT, string $encoding = null) : string
568
    {
569
        $encoding = $encoding ?: static::encoding($str);
570
571
        // Get the length of the input string - we'll need it to determine how much padding to apply.
572
        // Note: We're not returning early when the input string is empty - this is acceptable input for this
573
        // method. We will, however, return early when $with (the padding) is empty.
574
        $strLen  = mb_strlen($str, $encoding);
575
        $padding = $length - $strLen;
576
577
        // Determine how much padding to apply to either of the sides depending on which padding type
578
        // we were asked to perform.
579
        switch ($type) {
580
            case STR_PAD_LEFT:
581
                $left  = $padding;
582
                $right = 0;
583
                break;
584
585
            case STR_PAD_RIGHT:
586
                $left  = 0;
587
                $right = $padding;
588
                break;
589
590
            case STR_PAD_BOTH:
591
                $left  = floor($padding / 2);
592
                $right = ceil($padding / 2);
593
                break;
594
595
            default:
596
                throw new \InvalidArgumentException('Expected $type to be one of [STR_PAD_RIGHT|STR_PAD_LEFT|STR_PAD_BOTH], got ['.$type.'] instead.');
597
        }
598
599
        // If there's no actual padding or if the final length of the string would be longer than the
600
        // input string, we'll do nothing and return the input string.
601
        if (0 === $padLen = mb_strlen($with, $encoding) || $strLen >= $paddedLength = $strLen + $left + $right) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $padLen = (mb_strlen($wi...rLen + $left + $right)), Probably Intended Meaning: ($padLen = mb_strlen($wi...trLen + $left + $right)
Loading history...
602
            return $str;
603
        }
604
605
        // Construct the requested padding strings.
606
        $leftPadding  = 0 === $left  ? '' : mb_substr(str_repeat($with, ceil($left / $padLen)), 0, $left, $encoding);
607
        $rightPadding = 0 === $right ? '' : mb_substr(str_repeat($with, ceil($right / $padLen)), 0, $right, $encoding);
608
609
        // Apply the padding and return the glued string.
610
        return $leftPadding . $str . $rightPadding;
611
    }
612
613
    /**
614
     * Alias for self::pad() with the $type set to apply padding to both sides of the input string.
615
     *
616
     * @see Str::pad()
617
     */
618
    public static function padBoth(string $str, int $length, string $with = ' ', string $encoding = null) : string
619
    {
620
        return static::pad($str, $length, $with, STR_PAD_BOTH, $encoding);
621
    }
622
623
    /**
624
     * Alias for self::pad() with the $type set to apply padding only to the left side of the input string.
625
     *
626
     * @see Str::pad()
627
     */
628
    public static function padLeft(string $str, int $length, string $with = ' ', string $encoding = null) : string
629
    {
630
        return static::pad($str, $length, $with, STR_PAD_LEFT, $encoding);
631
    }
632
633
    /**
634
     * Alias for self::pad() with the $type set to apply padding only to the left side of the input string.
635
     *
636
     * Note: Self::pad() performs padding on the right side only by default - this alias is provided mostly
637
     * for consistency and verbosity.
638
     *
639
     * @see Str::pad()
640
     */
641
    public static function padRight(string $str, int $length, string $with = ' ', string $encoding = null) : string
642
    {
643
        return static::pad($str, $length, $with, STR_PAD_RIGHT, $encoding);
644
    }
645
646
    /** --
647
     *   Do *not* use this method in a cryptographic context without passing in a higher $strength
648
     *   parameter or better yet, use Random::string() directly instead.
649
     *  --
650
     *
651
     * Generates a pseudo-random string of the specified length using alpha-numeric characters
652
     * or the characters provided.
653
     *
654
     * @see     Random::string()
655
     *
656
     * @param   int         $length         The expected length of the generated string.
657
     * @param   string|int  $characters     The character list to use. Can be either a string
658
     *                                      with the characters to use or an int | nyx\core\Mask
659
     *                                      to generate a list (@see self::buildCharacterSet()).
660
     *                                      If not provided or an invalid mask, the method will fall
661
     *                                      back to a base alphanumeric character set.
662
     * @param   int         $strength       The requested strength of entropy (one of the Random::STRENGTH_*
663
     *                                      class constants)
664
     * @return  string                      The generated string.
665
     */
666
    public static function random(int $length = 8, $characters = null, int $strength = Random::STRENGTH_NONE) : string
667
    {
668
        // Note: We're duplicating ourselves by specifying the character pool directly instead of
669
        // relying on str\Character::buildSet(), but this skips this process in Random::string()
670
        // and is therefore faster.
671
        return Random::string($length, $characters ?: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', $strength);
672
    }
673
674
    /**
675
     * Removes the given $needle(s) from the $haystack.
676
     *
677
     * @param   string          $haystack   The string to remove from.
678
     * @param   string|string[] $needles    The substrings to remove.
679
     * @return  string                      The resulting string.
680
     */
681
    public static function remove(string $haystack, $needles) : string
682
    {
683
        return static::replace($haystack, $needles, '');
684
    }
685
686
    /**
687
     * Removes the given $needle from the beginning (only) of the $haystack. Only the first occurrence of the $needle
688
     * will be removed. If the $haystack does not start with the specified $needle, nothing will be removed.
689
     *
690
     * @param   string      $haystack   The string to remove from.
691
     * @param   string      $needle     The substring to remove from the beginning.
692
     * @param   string|null $encoding   The encoding to use.
693
     * @return  string                  The resulting string.
694
     */
695
    public static function removeLeft(string $haystack, string $needle, string $encoding = null) : string
696
    {
697
        // Early return in obvious circumstances.
698
        if ($haystack === '' || $needle === '') {
699
            return $haystack;
700
        }
701
702
        $encoding = $encoding ?: static::encoding($haystack);
703
704
        // This is a non-DRY version of self::startsWith(). If $haystack doesn't even start
705
        // with the given substring, we'll just return $haystack.
706
        if (0 !== mb_strpos($haystack, $needle, 0, $encoding) || 0 === $needleLen = mb_strlen($needle, $encoding)) {
707
            return $haystack;
708
        }
709
710
        // Grab a substring of the full initial string starting from the end of the prefix
711
        // we're cutting off... and return it.
712
        return mb_substr($haystack, $needleLen, null, $encoding);
713
    }
714
715
    /**
716
     * Removes the given $needle from the end (only) of the $haystack. Only the last occurrence of the $needle
717
     * will be removed. If the $haystack does not end with the specified $needle, nothing will be removed.
718
     *
719
     * @param   string      $haystack   The string to remove from.
720
     * @param   string      $needle     The substring to remove from the end.
721
     * @param   string|null $encoding   The encoding to use.
722
     * @return  string                  The resulting string.
723
     */
724
    public static function removeRight(string $haystack, string $needle, string $encoding = null) : string
725
    {
726
        // Early return in obvious circumstances.
727
        if ($haystack === '' || $needle === '') {
728
            return $haystack;
729
        }
730
731
        $encoding  = $encoding ?: static::encoding($haystack);
732
        $needleLen = mb_strlen($needle, $encoding);
733
734
        // This is a non-DRY version of self::endsWith(). If $haystack doesn't even end
735
        // with the given substring, we'll just return $haystack.
736
        if (0 === $needleLen || $needle !== mb_substr($haystack, -$needleLen, null, $encoding)) {
737
            return $haystack;
738
        }
739
740
        // Grab a substring of the full initial string ending at the beginning of the suffix
741
        // we're cutting off... and return it.
742
        return mb_substr($haystack, 0, mb_strlen($haystack, $encoding) - $needleLen, $encoding);
743
    }
744
745
    /**
746
     * Replaces the $needles within the given $haystack with the $replacement. If $needles is a string,
747
     * it will be treated as a regular expression. If it is an array, it will be treated as an array
748
     * of substrings to replace (an appropriate regular expression will be constructed).
749
     *
750
     * Acts as an utility alias for mb_ereg_replace().
751
     *
752
     * @param   string          $haystack       The string to replace $needles in.
753
     * @param   string|string[] $needles        What to replace in the string.
754
     * @param   string          $replacement    The replacement value.
755
     * @param   string          $options        The matching conditions as a string.
756
     *                                          {@link http://php.net/manual/en/function.mb-ereg-replace.php}
757
     * @param   string|null     $encoding       The encoding to use.
758
     * @return  string                          The resulting string.
759
     */
760
    public static function replace(string $haystack, $needles, string $replacement, string $options = 'msr', string $encoding = null) : string
761
    {
762
        // If multiple values to replace were passed.
763
        if (is_array($needles)) {
764
            $needles = '(' .implode('|', $needles). ')';
765
        }
766
767
        // Keep track of the internal encoding as we'll change it temporarily and then revert back to it.
768
        $internalEncoding = mb_regex_encoding();
769
770
        // Swap out the internal encoding for what we want...
771
        mb_regex_encoding($encoding ?: static::encoding($haystack));
772
773
        // ... and perform the replacement.
774
        $result = mb_ereg_replace($needles, $replacement, $haystack, $options);
775
776
        // Restore the initial internal encoding.
777
        mb_regex_encoding($internalEncoding);
778
779
        return $result;
780
    }
781
782
    /**
783
     * Replaces the first occurrence of each of the $needles in $haystack with $replacement.
784
     *
785
     * This method will search from the beginning of $haystack after processing each needle and replacing it,
786
     * meaning subsequent iterations may replace substrings resulting from previous iterations.
787
     *
788
     * @param   string          $haystack       The string to replace $needles in.
789
     * @param   string|string[] $needles        What to replace in the string.
790
     * @param   string          $replacement    The replacement value for each found (first) needle.
791
     * @param   bool            $strict         Whether to use case-sensitive comparisons.
792
     * @param   string|null     $encoding       The encoding to use.
793
     * @return  string                          The resulting string.
794
     */
795
    public static function replaceFirst(string $haystack, $needles, string $replacement, bool $strict = true, string $encoding = null) : string
796
    {
797
        return static::replaceOccurrence($haystack, $needles, $replacement, $strict, true, $encoding);
798
    }
799
800
    /**
801
     * Replaces the last occurrence of each of the $needles in $haystack with $replacement.
802
     *
803
     * This method will search from the end of $haystack after processing each needle and replacing it,
804
     * meaning subsequent iterations may replace substrings resulting from previous iterations.
805
     *
806
     * @param   string          $haystack       The string to replace $needles in.
807
     * @param   string|string[] $needles        What to replace in the string.
808
     * @param   string          $replacement    The replacement value for each found (last) needle.
809
     * @param   bool            $strict         Whether to use case-sensitive comparisons.
810
     * @param   string|null     $encoding       The encoding to use.
811
     * @return  string                          The resulting string.
812
     */
813
    public static function replaceLast(string $haystack, $needles, string $replacement, bool $strict = true, string $encoding = null) : string
814
    {
815
        return static::replaceOccurrence($haystack, $needles, $replacement, $strict, false, $encoding);
816
    }
817
818
    /**
819
     * Reverses a string. Multi-byte-safe equivalent of strrev().
820
     *
821
     * @param   string      $str        The string to reverse.
822
     * @param   string|null $encoding   The encoding to use.
823
     * @return  string                  The resulting string.
824
     */
825
    public static function reverse(string $str, string $encoding = null) : string
826
    {
827
        $encoding = $encoding ?: static::encoding($str);
828
        $length   = mb_strlen($str, $encoding);
829
        $result   = '';
830
831
        // Return early under obvious circumstances.
832
        if ($length === 0) {
833
            return $result;
834
        }
835
836
        // Reverse one character after the other, counting from the end.
837 View Code Duplication
        for ($i = $length - 1; $i >= 0; $i--) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
838
            $result .= mb_substr($str, $i, 1, $encoding);
839
        }
840
841
        return $result;
842
    }
843
844
    /**
845
     * Randomizes the order of characters in the given string. Multi-byte-safe equivalent
846
     * of str_shuffle().
847
     *
848
     * @param   string      $str        The string to shuffle.
849
     * @param   string|null $encoding   The encoding to use.
850
     * @return  string                  The resulting string.
851
     */
852
    public static function shuffle(string $str, string $encoding = null) : string
853
    {
854
        if ($str === '') {
855
            return $str;
856
        }
857
858
        $result   = '';
859
        $encoding = $encoding ?: static::encoding($str);
860
        $indices  = range(0, mb_strlen($str, $encoding) - 1);
861
862
        shuffle($indices);
863
864
        foreach ($indices as $i) {
865
            $result .= mb_substr($str, $i, 1, $encoding);
866
        }
867
868
        return $result;
869
    }
870
871
    /**
872
     * Generates a URL-friendly slug from the given string.
873
     *
874
     * @param   string  $str        The string to slugify.
875
     * @param   string  $delimiter  The delimiter to replace non-alphanumeric characters with.
876
     * @return  string              The resulting slug.
877
     */
878
    public static function slug(string $str, string $delimiter = '-') : string
879
    {
880
        $str = static::toAscii($str);
881
882
        // Remove all characters that are neither alphanumeric, nor the separator nor a whitespace.
883
        $str = preg_replace('![^'.preg_quote($delimiter).'\pL\pN\s]+!u', '', mb_strtolower($str));
884
885
        // Standardize the separator.
886
        $flip = $delimiter == '-' ? '_' : '-';
887
        $str = preg_replace('!['.preg_quote($flip).']+!u', $delimiter, $str);
888
889
        // Replace all separator characters and whitespace by a single separator.
890
        $str = preg_replace('!['.preg_quote($delimiter).'\s]+!u', $delimiter, $str);
891
892
        return trim($str, $delimiter);
893
    }
894
895
    /**
896
     * Determines whether the given $haystack starts with the given needle or one of the given needles
897
     * if $needles is an array.
898
     *
899
     * @param   string          $haystack   The string to search in.
900
     * @param   string|string[] $needles    The needle(s) to look for.
901
     * @param   bool            $strict     Whether to use case-sensitive comparisons.
902
     * @param   string|null     $encoding   The encoding to use.
903
     * @return  bool                        True when the string starts with one of the given needles, false otherwise.
904
     */
905
    public static function startsWith(string $haystack, $needles, bool $strict = true, string $encoding = null) : bool
906
    {
907
        if ($haystack === '') {
908
            return false;
909
        }
910
911
        $encoding = $encoding ?: static::encoding($haystack);
912
        $func     = $strict ? 'mb_strpos' : 'mb_stripos';
913
914
        foreach ((array) $needles as $needle) {
915
            if ($needle !== '' && 0 === $func($haystack, $needle, 0, $encoding)) {
916
                return true;
917
            }
918
        }
919
920
        return false;
921
    }
922
923
    /**
924
     * Returns a part of the given $haystack starting at the given $offset.
925
     *
926
     * @param   string      $haystack       The input string.
927
     * @param   int         $offset         The offset at which to start the slice. If a negative offset is given,
928
     *                                      the slice will start at the $offset-th character counting from the end
929
     *                                      of the input string.
930
     * @param   int|null    $length         The length of the slice. Must be a positive integer or null. If null,
931
     *                                      the full string starting from $offset will be returned. If a length
932
     *                                      which is longer than the input string is requested the method will
933
     *                                      silently ignore this and will act as if null was passed as length.
934
     * @param   string|null $encoding       The encoding to use.
935
     * @return  string                      The resulting string.
936
     * @throws  \InvalidArgumentException   When $length is negative.
937
     * @throws  \OutOfBoundsException       When the $start index is not contained in the input string.
938
     */
939
    public static function sub(string $haystack, int $offset, int $length = null, string $encoding = null) : string
940
    {
941
        if ($length === 0) {
942
            return '';
943
        }
944
945
        // We could silently return the initial string, but a negative $length may be an indicator of
946
        // mismatching $start with $length in the method call.
947
        if ($length < 0) {
948
            throw new \InvalidArgumentException('The length of the requested substring must be > 0, ['.$length.'] requested.');
949
        }
950
951
        $encoding = $encoding ?: static::encoding($haystack);
952
953
        // Check if the absolute starting index (to account for negative indices) + 1 (since it's 0-indexed
954
        // while length is > 1 at this point) is within the length of the string.
955 View Code Duplication
        if (abs($offset) >= mb_strlen($haystack, $encoding)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
956
            throw new \OutOfBoundsException('The given $offset ['.$offset.'] does not exist within the string ['.static::truncate($haystack, 20, '...', false, $encoding).'].');
957
        }
958
959
        return mb_substr($haystack, $offset, $length, $encoding);
960
    }
961
962
    /**
963
     * Surrounds the $haystack with the given $needle.
964
     *
965
     * Works consistent with Str::begin() and Str::finish() in that it ensures only single instances of
966
     * the $needle will be present at the beginning and end of the $haystack.
967
     *
968
     * @param   string  $haystack   The string to surround.
969
     * @param   string  $needle     The substring to surround the string with.
970
     * @return  string              The resulting string.
971
     */
972
    public static function surroundWith(string $haystack, string $needle) : string
973
    {
974
        return static::beginWith($haystack, $needle) . $needle . static::finishWith($haystack, $needle);
975
    }
976
977
    /**
978
     * Alias for @see Str::characters()
979
     */
980
    public static function toArray(string $str, string $encoding = null) : array
981
    {
982
        return static::characters($str, $encoding);
983
    }
984
985
    /**
986
     * Transliterates an UTF-8 encoded string to its ASCII equivalent.
987
     *
988
     * @param   string  $str    The UTF-8 encoded string to transliterate.
989
     * @return  string          The ASCII equivalent of the input string.
990
     */
991
    public static function toAscii(string $str) : string
992
    {
993
        if (preg_match("/[\x80-\xFF]/", $str)) {
994
            // Grab the transliteration table since we'll need it.
995
            if (null === static::$ascii) {
996
                static::$ascii = unserialize(file_get_contents(__DIR__ . '/str/resources/transliteration_table.ser'));
997
            }
998
999
            $str = \Normalizer::normalize($str, \Normalizer::NFKD);
1000
            $str = preg_replace('/\p{Mn}+/u', '', $str);
1001
            $str = str_replace(static::$ascii[0], static::$ascii[1], $str);
1002
            $str = iconv('UTF-8', 'ASCII' . ('glibc' !== ICONV_IMPL ? '//IGNORE' : '') . '//TRANSLIT', $str);
1003
        }
1004
1005
        return $str;
1006
    }
1007
1008
    /**
1009
     * Checks whether the given string represents a boolean value. Case insensitive.
1010
     *
1011
     * Works different than simply casting a string to a bool in that strings like "yes"/"no",
1012
     * "y"/"n", "on"/"off", "1"/"0" and "true"/"false" are interpreted based on the natural language
1013
     * value they represent.
1014
     *
1015
     * Numeric strings are left as in native PHP typecasting, ie. only 0 represents false. Every
1016
     * other numeric string, including negative numbers, will be treated as a truthy value.
1017
     *
1018
     * Non-empty strings containing only whitespaces, tabs or newlines will also be interpreted
1019
     * as empty (false) strings. Other than that normal typecasting rules apply.
1020
     *
1021
     * @param   string      $str        The string to check.
1022
     * @param   string|null $encoding   The encoding to use.
1023
     * @return  bool                    True when the string represents a boolean value, false otherwise.
1024
     * @todo    Grab the map from a separate method to allow easier extending with locale specific values?
1025
     * @todo    Rename to asBool() to make a more explicit distinction between this and normal typecasting?
1026
     */
1027
    public static function toBool(string $str, string $encoding = null) : bool
1028
    {
1029
        static $map = [
1030
            'true'  => true,
1031
            'false' => false,
1032
            '1'     => true,
1033
            '0'     => false,
1034
            'on'    => true,
1035
            'off'   => false,
1036
            'yes'   => true,
1037
            'no'    => false,
1038
            'y'     => true,
1039
            'n'     => false
1040
        ];
1041
1042
        $encoding = $encoding ?: static::encoding($str);
1043
1044
        return $map[mb_strtolower($str, $encoding)] ?? (bool) static::replace($str, '[[:space:]]', '', 'msr', $encoding);
1045
    }
1046
1047
    /**
1048
     * Converts each tab in the given string to the defined number of spaces (4 by default).
1049
     *
1050
     * @param   string  $str        The string in which to convert the tabs to whitespaces.
1051
     * @param   int     $length     The number of spaces to replace each tab with.
1052
     * @return  string              The resulting string.
1053
     */
1054
    public static function toSpaces(string $str, int $length = 4) : string
1055
    {
1056
        return str_replace("\t", str_repeat(' ', $length), $str);
1057
    }
1058
1059
    /**
1060
     * Converts each occurrence of the defined consecutive number of spaces (4 by default) to a tab.
1061
     *
1062
     * @param   string  $str        The string in which to convert the whitespaces to tabs.
1063
     * @param   int     $length     The number of consecutive spaces to replace with a tab.
1064
     * @return  string              The resulting string.
1065
     */
1066
    public static function toTabs(string $str, int $length = 4) : string
1067
    {
1068
        return str_replace(str_repeat(' ', $length), "\t", $str);
1069
    }
1070
1071
    /**
1072
     * Removes whitespaces (or other characters, if given) from the beginning and end of the given string.
1073
     * Handles multi-byte whitespaces.
1074
     *
1075
     * Note: If you simply want to remove whitespace and multi-byte whitespaces are of no concern,
1076
     * use PHP's native trim() instead, for obvious performance reasons.
1077
     *
1078
     * @param   string      $str        The string in which to convert the whitespaces to tabs.
1079
     * @param   string      $characters Optional characters to strip off (instead of the default whitespace chars).
1080
     * @param   string|null $encoding   The encoding to use.
1081
     * @return  string                  The resulting string.
1082
     */
1083
    public static function trim(string $str, string $characters = null, string $encoding = null) : string
1084
    {
1085
        $characters = null !== $characters ? preg_quote($characters) : '[:space:]';
1086
1087
        return static::replace($str, "^[$characters]+|[$characters]+\$", '', 'msr', $encoding);
1088
    }
1089
1090
    /**
1091
     * Removes whitespaces (or other characters, if given) from the beginning of the given string.
1092
     * Handles multi-byte whitespaces.
1093
     *
1094
     * Note: If you simply want to remove whitespace and multi-byte whitespaces are of no concern,
1095
     * use PHP's native ltrim() instead, for obvious performance reasons.
1096
     *
1097
     * @param   string      $str        The string in which to convert the whitespaces to tabs.
1098
     * @param   string      $characters Optional characters to strip off (instead of the default whitespace chars).
1099
     * @param   string|null $encoding   The encoding to use.
1100
     * @return  string                  The resulting string.
1101
     */
1102
    public static function trimLeft(string $str, string $characters = null, string $encoding = null) : string
1103
    {
1104
        $characters = null !== $characters ? preg_quote($characters) : '[:space:]';
1105
1106
        return static::replace($str, "^[$characters]+", '', 'msr', $encoding);
1107
    }
1108
1109
    /**
1110
     * Removes whitespaces (or other characters, if given) from the end of the given string.
1111
     * Handles multi-byte whitespaces.
1112
     *
1113
     * Note: If you simply want to remove whitespace and multi-byte whitespaces are of no concern,
1114
     * use PHP's native rtrim() instead, for obvious performance reasons.
1115
     *
1116
     * @param   string      $str        The string in which to convert the whitespaces to tabs.
1117
     * @param   string      $characters Optional characters to strip off (instead of the default whitespace chars).
1118
     * @param   string|null $encoding   The encoding to use.
1119
     * @return  string                  The resulting string.
1120
     */
1121
    public static function trimRight(string $str, string $characters = null, string $encoding = null) : string
1122
    {
1123
        $characters = null !== $characters ? preg_quote($characters) : '[:space:]';
1124
1125
        return static::replace($str, "[$characters]+\$", '', 'msr', $encoding);
1126
    }
1127
1128
    /**
1129
     * Trims the string to the given length, replacing the cut off characters from the end with an optional
1130
     * substring ("..." by default). The final length of the string, including the optionally appended $end
1131
     * substring, will not exceed $limit.
1132
     *
1133
     * @param   string      $str            The string to truncate.
1134
     * @param   int         $limit          The maximal number of characters to be contained in the string. Must be
1135
     *                                      a positive integer. If 0 is given, an empty string will be returned.
1136
     * @param   string      $end            The replacement for the whole of the cut off string (if any).
1137
     * @param   bool        $preserveWords  Whether to preserve words, ie. allow splitting only on whitespace
1138
     *                                      characters.
1139
     * @param   string|null $encoding       The encoding to use.
1140
     * @return  string                      The resulting string.
1141
     * @throws  \InvalidArgumentException   When $limit is a negative integer.
1142
     */
1143
    public static function truncate(string $str, int $limit = 100, string $end = '...', bool $preserveWords = false, string $encoding = null) : string
1144
    {
1145
        if ($limit < 0) {
1146
            throw new \InvalidArgumentException('The $limit must be a positive integer but ['.$limit.'] was given.');
1147
        }
1148
1149
        if ($limit === 0) {
1150
            return '';
1151
        }
1152
1153
        $encoding = $encoding ?: static::encoding($str);
1154
1155
        // Is there anything to actually truncate?
1156
        if (mb_strlen($str, $encoding) <= $limit) {
1157
            return $str;
1158
        }
1159
1160
        // Determine the final length of the substring of $str we might return and grab it.
1161
        $length = $limit - mb_strlen($end, $encoding);
1162
        $result = mb_substr($str, 0, $length, $encoding);
1163
1164
        // If we are to preserve words, see whether the last word got truncated by checking if
1165
        // the truncated string would've been directly followed by a whitespace or not. If not,
1166
        // we're going to get the position of the last whitespace in the truncated string and
1167
        // cut the whole thing off at that offset instead.
1168
        if (true === $preserveWords && $length !== mb_strpos($str, ' ', $length - 1, $encoding)) {
1169
            $result = mb_substr($result, 0, mb_strrpos($result, ' ', 0, $encoding), $encoding);
1170
        }
1171
1172
        return $result . $end;
1173
    }
1174
1175
    /**
1176
     * Limits the number of words in the given string.
1177
     *
1178
     * @param   string      $str        The string to limit.
1179
     * @param   int         $words      The maximal number of words to be contained in the string, not counting
1180
     *                                  the replacement.
1181
     * @param   string|null $encoding   The encoding to use.
1182
     * @param   string      $end        The replacement for the whole of the cut off string (if any).
1183
     * @return  string                  The resulting string.
1184
     */
1185
    public static function words(string $str, int $words = 100, string $encoding = null, string $end = '...') : string
1186
    {
1187
        $encoding = $encoding ?: static::encoding($str);
1188
1189
        preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/u', $str, $matches);
1190
1191
        if (!isset($matches[0]) || mb_strlen($str, $encoding) === mb_strlen($matches[0], $encoding)) {
1192
            return $str;
1193
        }
1194
1195
        return rtrim($matches[0]).$end;
1196
    }
1197
1198
    /**
1199
     * Finds the first occurrence of $needle within $haystack and returns the part of $haystack
1200
     * before up to the beginning of the $needle or from the beginning of the $needle and including it,
1201
     * depending on whether $before is true or false.
1202
     *
1203
     * Used internally by {@see self::after()}, {@see self::before()} and {@see self::from()}.
1204
     *
1205
     * @param   string      $haystack   The string to search in.
1206
     * @param   string      $needle     The substring to search for.
1207
     * @param   bool        $strict     Whether to use case-sensitive comparisons. True by default.
1208
     * @param   bool        $before     Whether to return the part of $haystack before $needle or part of
1209
     *                                  $haystack starting at $needle and including $needle.
1210
     * @param   string|null $encoding   The encoding to use.
1211
     * @return  string                  The part of $haystack before/from $needle.
1212
     * @throws  \RuntimeException       Upon failing to find $needle in $haystack at all.
1213
     */
1214
    protected static function partOf(string $haystack, string $needle, bool $strict, bool $before, string $encoding = null) : string
1215
    {
1216
        $func     = $strict ? 'mb_strstr' : 'mb_stristr';
1217
        $encoding = $encoding ?: static::encoding($haystack);
1218
1219 View Code Duplication
        if (false === $result = $func($haystack, $needle, $before, $encoding)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1220
            throw new \RuntimeException('Failed to find $needle ['.$needle.'] in $haystack ['.static::truncate($haystack, 20, '...', false, $encoding).'].');
1221
        }
1222
1223
        return $result;
1224
    }
1225
1226
    /**
1227
     * Replaces a single occurrence of each of the $needles in $haystack with $replacement - either the first or
1228
     * the last occurrence, depending whether $first is true or false.
1229
     *
1230
     * This method will search from the beginning/end of $haystack after processing each needle and replacing it,
1231
     * meaning subsequent iterations may replace substrings resulting from previous iterations.
1232
     *
1233
     * Used internally by {@see self::replaceFirst()} and {@see self::replaceLast()}.
1234
     *
1235
     * @param   string          $haystack       The string to replace $needles in.
1236
     * @param   string|string[] $needles        What to replace in the string.
1237
     * @param   string          $replacement    The replacement value for each found (last) needle.
1238
     * @param   bool            $strict         Whether to use case-sensitive comparisons.
1239
     * @param   bool            $first          Whether to replace the first or the last occurrence of the needle(s).
1240
     * @param   string|null     $encoding       The encoding to use.
1241
     * @return  string                          The resulting string.
1242
     */
1243
    protected static function replaceOccurrence(string $haystack, $needles, string $replacement, bool $strict, bool $first, string $encoding = null) : string
1244
    {
1245
        if ($haystack === '') {
1246
            return '';
1247
        }
1248
1249
        $encoding = $encoding ?: static::encoding($haystack);
1250
        $method   = $first    ? 'indexOf' : 'indexOfLast';
1251
1252
        foreach ((array) $needles as $needle) {
1253
1254
            // Pass to the next needle if this one is an empty string or if it couldn't be found
1255
            // in the haystack at all.
1256
            if ($needle === '' || -1 === $offset = static::$method($haystack, $needle, 0, $strict, $encoding) || 0 === $needleLen = mb_strlen($needle, $encoding)) {
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $offset = (static::$meth...n($needle, $encoding))), Probably Intended Meaning: ($offset = static::$meth...en($needle, $encoding))
Loading history...
1257
                continue;
1258
            }
1259
1260
            // Grab the substrings before and after the needle occurs, insert the replacement in between
1261
            // and glue it together omitting the needle.
1262
            $haystack = mb_substr($haystack, 0, $offset, $encoding) . $replacement . mb_substr($haystack, $offset + $needleLen, null, $encoding);
0 ignored issues
show
The variable $needleLen does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1263
        }
1264
1265
        return $haystack;
1266
    }
1267
}
1268