Completed
Push — master ( cbeae3...e1f0ba )
by Lars
01:56
created

Stringy::pad()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 4
nop 3
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
ccs 9
cts 9
cp 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stringy;
6
7
use voku\helper\AntiXSS;
8
use voku\helper\EmailCheck;
9
use voku\helper\URLify;
10
use voku\helper\UTF8;
11
12
/**
13
 * Class Stringy
14
 *
15
 * @package Stringy
16
 */
17
class Stringy implements \Countable, \IteratorAggregate, \ArrayAccess
18
{
19
  /**
20
   * An instance's string.
21
   *
22
   * @var string
23
   */
24
  protected $str;
25
26
  /**
27
   * The string's encoding, which should be one of the mbstring module's
28
   * supported encodings.
29
   *
30
   * @var string
31
   */
32
  protected $encoding;
33
34
  /**
35
   * Initializes a Stringy object and assigns both str and encoding properties
36
   * the supplied values. $str is cast to a string prior to assignment, and if
37
   * $encoding is not specified, it defaults to mb_internal_encoding(). Throws
38
   * an InvalidArgumentException if the first argument is an array or object
39
   * without a __toString method.
40
   *
41
   * @param mixed  $str      [optional] <p>Value to modify, after being cast to string. Default: ''</p>
42
   * @param string $encoding [optional] <p>The character encoding. Fallback: 'UTF-8'</p>
43
   *
44
   * @throws \InvalidArgumentException <p>if an array or object without a
45
   *         __toString method is passed as the first argument</p>
46
   */
47 1152
  public function __construct($str = '', string $encoding = null)
48
  {
49 1152
    if (\is_array($str)) {
50 1
      throw new \InvalidArgumentException(
51 1
          'Passed value cannot be an array'
52
      );
53
    }
54
55
    if (
56 1151
        \is_object($str)
57
        &&
58 1151
        !\method_exists($str, '__toString')
59
    ) {
60 1
      throw new \InvalidArgumentException(
61 1
          'Passed object must have a __toString method'
62
      );
63
    }
64
65 1150
    $this->str = (string)$str;
66
67 1150
    if ($encoding) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encoding of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
68 1143
      $this->encoding = UTF8::normalize_encoding($encoding);
69
    } else {
70 10
      $this->encoding = 'UTF-8';
71
    }
72 1150
  }
73
74
  /**
75
   * Returns the value in $str.
76
   *
77
   * @return string <p>The current value of the $str property.</p>
78
   */
79 221
  public function __toString()
80
  {
81 221
    return (string)$this->str;
82
  }
83
84
  /**
85
   * Returns a new string with $string appended.
86
   *
87
   * @param string $string <p>The string to append.</p>
88
   *
89
   * @return static <p>Object with appended $string.</p>
90
   */
91 5
  public function append(string $string): self
92
  {
93 5
    return static::create($this->str . $string, $this->encoding);
94
  }
95
96
  /**
97
   * Append an password (limited to chars that are good readable).
98
   *
99
   * @param int $length <p>Length of the random string.</p>
100
   *
101
   * @return static <p>Object with appended password.</p>
102
   */
103 1
  public function appendPassword(int $length): self
104
  {
105 1
    $possibleChars = '2346789bcdfghjkmnpqrtvwxyzBCDFGHJKLMNPQRTVWXYZ';
106
107 1
    return $this->appendRandomString($length, $possibleChars);
108
  }
109
110
  /**
111
   * Append an unique identifier.
112
   *
113
   * @param string|int $entropyExtra [optional] <p>Extra entropy via a string or int value.</p>
114
   * @param bool       $md5          [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
115
   *
116
   * @return static <p>Object with appended unique identifier as md5-hash.</p>
117
   */
118 1
  public function appendUniqueIdentifier($entropyExtra = '', bool $md5 = true): self
119
  {
120 1
    $uniqueHelper = \mt_rand() .
121 1
                    \session_id() .
122 1
                    ($_SERVER['REMOTE_ADDR'] ?? '') .
123 1
                    ($_SERVER['SERVER_ADDR'] ?? '') .
124 1
                    $entropyExtra;
125
126 1
    $uniqueString = \uniqid($uniqueHelper, true);
127
128 1
    if ($md5) {
129 1
      $uniqueString = \md5($uniqueString . $uniqueHelper);
130
    }
131
132 1
    return $this->append($uniqueString);
133
  }
134
135
  /**
136
   * Append an random string.
137
   *
138
   * @param int    $length        <p>Length of the random string.</p>
139
   * @param string $possibleChars [optional] <p>Characters string for the random selection.</p>
140
   *
141
   * @return static <p>Object with appended random string.</p>
142
   */
143 2
  public function appendRandomString(int $length, string $possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'): self
144
  {
145
    // init
146 2
    $i = 0;
147 2
    $str = $this->str;
148 2
    $maxlength = UTF8::strlen($possibleChars, $this->encoding);
149
150 2
    if ($maxlength === 0) {
151 1
      return $this;
152
    }
153
154
    // add random chars
155 2
    while ($i < $length) {
156 2
      $char = UTF8::substr($possibleChars, \random_int(0, $maxlength - 1), 1, $this->encoding);
157 2
      $str .= $char;
158 2
      $i++;
159
    }
160
161 2
    return $this->append($str);
162
  }
163
164
  /**
165
   * Creates a Stringy object and assigns both str and encoding properties
166
   * the supplied values. $str is cast to a string prior to assignment, and if
167
   * $encoding is not specified, it defaults to mb_internal_encoding(). It
168
   * then returns the initialized object. Throws an InvalidArgumentException
169
   * if the first argument is an array or object without a __toString method.
170
   *
171
   * @param  mixed  $str      [optional] <p>Value to modify, after being cast to string. Default: ''</p>
172
   * @param  string $encoding [optional] <p>The character encoding. Fallback: 'UTF-8'</p>
173
   *
174
   * @return static <p>A Stringy object.</p>
175
   *
176
   * @throws \InvalidArgumentException <p>if an array or object without a
177
   *         __toString method is passed as the first argument</p>
178
   */
179 1142
  public static function create($str = '', string $encoding = null): self
180
  {
181 1142
    if ($encoding) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encoding of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
182 898
      $encoding = UTF8::normalize_encoding($encoding);
183
    } else {
184 766
      $encoding = 'UTF-8';
185
    }
186
187 1142
    return new static($str, $encoding);
188
  }
189
190
  /**
191
   * Returns the substring between $start and $end, if found, or an empty
192
   * string. An optional offset may be supplied from which to begin the
193
   * search for the start string.
194
   *
195
   * @param  string $start  <p>Delimiter marking the start of the substring.</p>
196
   * @param  string $end    <p>Delimiter marking the end of the substring.</p>
197
   * @param  int    $offset [optional] <p>Index from which to begin the search. Default: 0</p>
198
   *
199
   * @return static <p>Object whose $str is a substring between $start and $end.</p>
200
   */
201 16
  public function between(string $start, string $end, int $offset = 0): self
202
  {
203 16
    $startIndex = $this->indexOf($start, $offset);
204 16
    if ($startIndex === false) {
205 2
      return static::create('', $this->encoding);
206
    }
207
208 14
    $substrIndex = $startIndex + UTF8::strlen($start, $this->encoding);
209 14
    $endIndex = $this->indexOf($end, $substrIndex);
210 14
    if ($endIndex === false) {
211 2
      return static::create('', $this->encoding);
212
    }
213
214 12
    return $this->substr($substrIndex, $endIndex - $substrIndex);
215
  }
216
217
  /**
218
   * Returns the index of the first occurrence of $needle in the string,
219
   * and false if not found. Accepts an optional offset from which to begin
220
   * the search.
221
   *
222
   * @param  string $needle <p>Substring to look for.</p>
223
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
224
   *
225
   * @return int|false <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
226
   */
227 29
  public function indexOf(string $needle, int $offset = 0)
228
  {
229 29
    return UTF8::strpos($this->str, $needle, $offset, $this->encoding);
230
  }
231
232
  /**
233
   * Returns the index of the first occurrence of $needle in the string,
234
   * and false if not found. Accepts an optional offset from which to begin
235
   * the search.
236
   *
237
   * @param  string $needle <p>Substring to look for.</p>
238
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
239
   *
240
   * @return int|false <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
241
   */
242 2
  public function indexOfIgnoreCase(string $needle, int $offset = 0)
243
  {
244 2
    return UTF8::stripos($this->str, $needle, $offset, $this->encoding);
245
  }
246
247
  /**
248
   * Returns the substring beginning at $start with the specified $length.
249
   * It differs from the UTF8::substr() function in that providing a $length of
250
   * null will return the rest of the string, rather than an empty string.
251
   *
252
   * @param int $start  <p>Position of the first character to use.</p>
253
   * @param int $length [optional] <p>Maximum number of characters used. Default: null</p>
254
   *
255
   * @return static <p>Object with its $str being the substring.</p>
256
   */
257 65
  public function substr(int $start, int $length = null): self
258
  {
259 65
    if ($length === null) {
260 19
      $length = $this->length();
261
    }
262
263 65
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
264
265 65
    return static::create($str, $this->encoding);
266
  }
267
268
  /**
269
   * Returns the length of the string.
270
   *
271
   * @return int <p>The number of characters in $str given the encoding.</p>
272
   */
273 293
  public function length(): int
274
  {
275 293
    return UTF8::strlen($this->str, $this->encoding);
276
  }
277
278
  /**
279
   * Trims the string and replaces consecutive whitespace characters with a
280
   * single space. This includes tabs and newline characters, as well as
281
   * multibyte whitespace such as the thin space and ideographic space.
282
   *
283
   * @return static <p>Object with a trimmed $str and condensed whitespace.</p>
284
   */
285 52
  public function collapseWhitespace(): self
286
  {
287 52
    return $this->regexReplace('[[:space:]]+', ' ')->trim();
288
  }
289
290
  /**
291
   * Returns a string with whitespace removed from the start and end of the
292
   * string. Supports the removal of unicode whitespace. Accepts an optional
293
   * string of characters to strip instead of the defaults.
294
   *
295
   * @param string $chars [optional] <p>String of characters to strip. Default: null</p>
296
   *
297
   * @return static <p>Object with a trimmed $str.</p>
298
   */
299 188 View Code Duplication
  public function trim(string $chars = null): self
0 ignored issues
show
Duplication introduced by
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...
300
  {
301 188
    if (!$chars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chars of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
302 187
      $chars = '[:space:]';
303
    } else {
304 1
      $chars = \preg_quote($chars, '/');
305
    }
306
307 188
    return $this->regexReplace("^[$chars]+|[$chars]+\$", '');
308
  }
309
310
  /**
311
   * Replaces all occurrences of $pattern in $str by $replacement.
312
   *
313
   * @param  string $pattern     <p>The regular expression pattern.</p>
314
   * @param  string $replacement <p>The string to replace with.</p>
315
   * @param  string $options     [optional] <p>Matching conditions to be used.</p>
316
   * @param  string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
317
   *
318
   * @return static <p>Object with the result2ing $str after the replacements.</p>
319
   */
320 259
  public function regexReplace(string $pattern, string $replacement, string $options = '', string $delimiter = '/'): self
321
  {
322 259
    if ($options === 'msr') {
323 9
      $options = 'ms';
324
    }
325
326
    // fallback
327 259
    if (!$delimiter) {
328
      $delimiter = '/';
329
    }
330
331 259
    $str = (string)\preg_replace(
332 259
        $delimiter . $pattern . $delimiter . 'u' . $options,
333 259
        $replacement,
334 259
        $this->str
335
    );
336
337 259
    return static::create($str, $this->encoding);
338
  }
339
340
  /**
341
   * Returns true if the string contains all $needles, false otherwise. By
342
   * default the comparison is case-sensitive, but can be made insensitive by
343
   * setting $caseSensitive to false.
344
   *
345
   * @param  array $needles       <p>SubStrings to look for.</p>
346
   * @param  bool  $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
347
   *
348
   * @return bool  <p>Whether or not $str contains $needle.</p>
349
   */
350 43 View Code Duplication
  public function containsAll(array $needles, bool $caseSensitive = true): bool
0 ignored issues
show
Duplication introduced by
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...
351
  {
352
    /** @noinspection IsEmptyFunctionUsageInspection */
353 43
    if (empty($needles)) {
354 1
      return false;
355
    }
356
357 42
    foreach ($needles as $needle) {
358 42
      if (!$this->contains($needle, $caseSensitive)) {
359 42
        return false;
360
      }
361
    }
362
363 24
    return true;
364
  }
365
366
  /**
367
   * Returns true if the string contains $needle, false otherwise. By default
368
   * the comparison is case-sensitive, but can be made insensitive by setting
369
   * $caseSensitive to false.
370
   *
371
   * @param string $needle        <p>Substring to look for.</p>
372
   * @param bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
373
   *
374
   * @return bool   <p>Whether or not $str contains $needle.</p>
375
   */
376 105
  public function contains(string $needle, bool $caseSensitive = true): bool
377
  {
378 105
    $encoding = $this->encoding;
379
380 105
    if ($caseSensitive) {
381 55
      return (UTF8::strpos($this->str, $needle, 0, $encoding) !== false);
382
    }
383
384 50
    return (UTF8::stripos($this->str, $needle, 0, $encoding) !== false);
385
  }
386
387
  /**
388
   * Returns true if the string contains any $needles, false otherwise. By
389
   * default the comparison is case-sensitive, but can be made insensitive by
390
   * setting $caseSensitive to false.
391
   *
392
   * @param  array $needles       <p>SubStrings to look for.</p>
393
   * @param  bool  $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
394
   *
395
   * @return bool <p>Whether or not $str contains $needle.</p>
396
   */
397 43 View Code Duplication
  public function containsAny(array $needles, bool $caseSensitive = true): bool
0 ignored issues
show
Duplication introduced by
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...
398
  {
399
    /** @noinspection IsEmptyFunctionUsageInspection */
400 43
    if (empty($needles)) {
401 1
      return false;
402
    }
403
404 42
    foreach ($needles as $needle) {
405 42
      if ($this->contains($needle, $caseSensitive)) {
406 42
        return true;
407
      }
408
    }
409
410 18
    return false;
411
  }
412
413
  /**
414
   * Returns the length of the string, implementing the countable interface.
415
   *
416
   * @return int <p>The number of characters in the string, given the encoding.</p>
417
   */
418 1
  public function count(): int
419
  {
420 1
    return $this->length();
421
  }
422
423
  /**
424
   * Returns the number of occurrences of $substring in the given string.
425
   * By default, the comparison is case-sensitive, but can be made insensitive
426
   * by setting $caseSensitive to false.
427
   *
428
   * @param  string $substring     <p>The substring to search for.</p>
429
   * @param  bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
430
   *
431
   * @return int|false <p>This functions returns an integer or false if there isn't a string.</p>
432
   */
433 15
  public function countSubstr(string $substring, bool $caseSensitive = true)
434
  {
435 15
    if ($caseSensitive) {
436 9
      return UTF8::substr_count($this->str, $substring, 0, null, $this->encoding);
437
    }
438
439 6
    $str = UTF8::strtoupper($this->str, $this->encoding);
440 6
    $substring = UTF8::strtoupper($substring, $this->encoding);
441
442 6
    return UTF8::substr_count($str, $substring, 0, null, $this->encoding);
443
  }
444
445
  /**
446
   * Returns a lowercase and trimmed string separated by dashes. Dashes are
447
   * inserted before uppercase characters (with the exception of the first
448
   * character of the string), and in place of spaces as well as underscores.
449
   *
450
   * @return static <p>Object with a dasherized $str</p>
451
   */
452 19
  public function dasherize(): self
453
  {
454 19
    return $this->delimit('-');
455
  }
456
457
  /**
458
   * Returns a lowercase and trimmed string separated by the given delimiter.
459
   * Delimiters are inserted before uppercase characters (with the exception
460
   * of the first character of the string), and in place of spaces, dashes,
461
   * and underscores. Alpha delimiters are not converted to lowercase.
462
   *
463
   * @param string $delimiter <p>Sequence used to separate parts of the string.</p>
464
   *
465
   * @return static <p>Object with a delimited $str.</p>
466
   */
467 49
  public function delimit(string $delimiter): self
468
  {
469 49
    $str = $this->trim();
470
471 49
    $str = (string)\preg_replace('/\B([A-Z])/u', '-\1', $str);
472
473 49
    $str = UTF8::strtolower($str, $this->encoding);
474
475 49
    $str = (string)\preg_replace('/[-_\s]+/u', $delimiter, $str);
476
477 49
    return static::create($str, $this->encoding);
478
  }
479
480
  /**
481
   * Ensures that the string begins with $substring. If it doesn't, it's
482
   * prepended.
483
   *
484
   * @param string $substring <p>The substring to add if not present.</p>
485
   *
486
   * @return static <p>Object with its $str prefixed by the $substring.</p>
487
   */
488 10 View Code Duplication
  public function ensureLeft(string $substring): self
0 ignored issues
show
Duplication introduced by
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...
489
  {
490 10
    $stringy = static::create($this->str, $this->encoding);
491
492 10
    if (!$stringy->startsWith($substring)) {
493 4
      $stringy->str = $substring . $stringy->str;
494
    }
495
496 10
    return $stringy;
497
  }
498
499
  /**
500
   * Returns true if the string begins with $substring, false otherwise. By
501
   * default, the comparison is case-sensitive, but can be made insensitive
502
   * by setting $caseSensitive to false.
503
   *
504
   * @param  string $substring     <p>The substring to look for.</p>
505
   * @param  bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
506
   *
507
   * @return bool   <p>Whether or not $str starts with $substring.</p>
508
   */
509 45
  public function startsWith(string $substring, bool $caseSensitive = true): bool
510
  {
511 45
    $str = $this->str;
512
513 45 View Code Duplication
    if (!$caseSensitive) {
0 ignored issues
show
Duplication introduced by
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...
514 8
      $substring = UTF8::strtolower($substring, $this->encoding);
515 8
      $str = UTF8::strtolower($this->str, $this->encoding);
516
    }
517
518 45
    return UTF8::strpos($str, $substring, 0, $this->encoding) === 0;
519
  }
520
521
  /**
522
   * Returns true if the string begins with any of $substrings, false otherwise.
523
   * By default the comparison is case-sensitive, but can be made insensitive by
524
   * setting $caseSensitive to false.
525
   *
526
   * @param  array $substrings    <p>Substrings to look for.</p>
527
   * @param  bool  $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
528
   *
529
   * @return bool  <p>Whether or not $str starts with $substring.</p>
530
   */
531 12 View Code Duplication
  public function startsWithAny(array $substrings, bool $caseSensitive = true): bool
0 ignored issues
show
Duplication introduced by
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...
532
  {
533 12
    if (empty($substrings)) {
534
      return false;
535
    }
536
537 12
    foreach ($substrings as $substring) {
538 12
      if ($this->startsWith($substring, $caseSensitive)) {
539 12
        return true;
540
      }
541
    }
542
543 6
    return false;
544
  }
545
546
  /**
547
   * Ensures that the string ends with $substring. If it doesn't, it's appended.
548
   *
549
   * @param string $substring <p>The substring to add if not present.</p>
550
   *
551
   * @return static <p>Object with its $str suffixed by the $substring.</p>
552
   */
553 10 View Code Duplication
  public function ensureRight(string $substring): self
0 ignored issues
show
Duplication introduced by
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...
554
  {
555 10
    $stringy = static::create($this->str, $this->encoding);
556
557 10
    if (!$stringy->endsWith($substring)) {
558 4
      $stringy->str .= $substring;
559
    }
560
561 10
    return $stringy;
562
  }
563
564
  /**
565
   * Returns true if the string ends with $substring, false otherwise. By
566
   * default, the comparison is case-sensitive, but can be made insensitive
567
   * by setting $caseSensitive to false.
568
   *
569
   * @param string $substring     <p>The substring to look for.</p>
570
   * @param bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
571
   *
572
   * @return bool   <p>Whether or not $str ends with $substring.</p>
573
   */
574 44
  public function endsWith(string $substring, bool $caseSensitive = true): bool
575
  {
576 44
    $substringLength = UTF8::strlen($substring, $this->encoding);
577 44
    $strLength = $this->length();
578
579 44
    $endOfStr = UTF8::substr(
580 44
        $this->str,
581 44
        $strLength - $substringLength,
582 44
        $substringLength,
583 44
        $this->encoding
584
    );
585
586 44 View Code Duplication
    if (!$caseSensitive) {
0 ignored issues
show
Duplication introduced by
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...
587 8
      $substring = UTF8::strtolower($substring, $this->encoding);
588 8
      $endOfStr = UTF8::strtolower($endOfStr, $this->encoding);
0 ignored issues
show
Security Bug introduced by
It seems like $endOfStr can also be of type false; however, voku\helper\UTF8::strtolower() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
589
    }
590
591 44
    return $substring === $endOfStr;
592
  }
593
594
  /**
595
   * Returns true if the string ends with any of $substrings, false otherwise.
596
   * By default, the comparison is case-sensitive, but can be made insensitive
597
   * by setting $caseSensitive to false.
598
   *
599
   * @param string[] $substrings    <p>Substrings to look for.</p>
600
   * @param bool     $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
601
   *
602
   * @return bool     <p>Whether or not $str ends with $substring.</p>
603
   */
604 11 View Code Duplication
  public function endsWithAny(array $substrings, bool $caseSensitive = true): bool
0 ignored issues
show
Duplication introduced by
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...
605
  {
606 11
    if (empty($substrings)) {
607
      return false;
608
    }
609
610 11
    foreach ($substrings as $substring) {
611 11
      if ($this->endsWith($substring, $caseSensitive)) {
612 11
        return true;
613
      }
614
    }
615
616 6
    return false;
617
  }
618
619
  /**
620
   * Returns the first $n characters of the string.
621
   *
622
   * @param int $n <p>Number of characters to retrieve from the start.</p>
623
   *
624
   * @return static <p>Object with its $str being the first $n chars.</p>
625
   */
626 13 View Code Duplication
  public function first(int $n): self
0 ignored issues
show
Duplication introduced by
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...
627
  {
628 13
    $stringy = static::create($this->str, $this->encoding);
629
630 13
    if ($n < 0) {
631 2
      $stringy->str = '';
632
    } else {
633 11
      return $stringy->substr(0, $n);
634
    }
635
636 2
    return $stringy;
637
  }
638
639
  /**
640
   * Returns the encoding used by the Stringy object.
641
   *
642
   * @return string <p>The current value of the $encoding property.</p>
643
   */
644 3
  public function getEncoding(): string
645
  {
646 3
    return $this->encoding;
647
  }
648
649
  /**
650
   * Returns a new ArrayIterator, thus implementing the IteratorAggregate
651
   * interface. The ArrayIterator's constructor is passed an array of chars
652
   * in the multibyte string. This enables the use of foreach with instances
653
   * of Stringy\Stringy.
654
   *
655
   * @return \ArrayIterator <p>An iterator for the characters in the string.</p>
656
   */
657 1
  public function getIterator(): \ArrayIterator
658
  {
659 1
    return new \ArrayIterator($this->chars());
660
  }
661
662
  /**
663
   * Returns an array consisting of the characters in the string.
664
   *
665
   * @return array <p>An array of string chars.</p>
666
   */
667 4
  public function chars(): array
668
  {
669
    // init
670 4
    $chars = [];
671 4
    $l = $this->length();
672
673 4
    for ($i = 0; $i < $l; $i++) {
674 3
      $chars[] = $this->at($i)->str;
675
    }
676
677 4
    return $chars;
678
  }
679
680
  /**
681
   * Returns the character at $index, with indexes starting at 0.
682
   *
683
   * @param int $index <p>Position of the character.</p>
684
   *
685
   * @return static <p>The character at $index.</p>
686
   */
687 11
  public function at(int $index): self
688
  {
689 11
    return $this->substr($index, 1);
690
  }
691
692
  /**
693
   * Returns true if the string contains a lower case char, false otherwise.
694
   *
695
   * @return bool <p>Whether or not the string contains a lower case character.</p>
696
   */
697 12
  public function hasLowerCase(): bool
698
  {
699 12
    return $this->matchesPattern('.*[[:lower:]]');
700
  }
701
702
  /**
703
   * Returns true if $str matches the supplied pattern, false otherwise.
704
   *
705
   * @param string $pattern <p>Regex pattern to match against.</p>
706
   *
707
   * @return bool <p>Whether or not $str matches the pattern.</p>
708
   */
709 103
  protected function matchesPattern(string $pattern): bool
710
  {
711 103
    if (\preg_match('/' . $pattern . '/u', $this->str)) {
712 64
      return true;
713
    }
714
715 39
    return false;
716
  }
717
718
  /**
719
   * Returns true if the string contains an upper case char, false otherwise.
720
   *
721
   * @return bool <p>Whether or not the string contains an upper case character.</p>
722
   */
723 12
  public function hasUpperCase(): bool
724
  {
725 12
    return $this->matchesPattern('.*[[:upper:]]');
726
  }
727
728
  /**
729
   * Convert all HTML entities to their applicable characters.
730
   *
731
   * @param int $flags       [optional] <p>
732
   *                         A bitmask of one or more of the following flags, which specify how to handle quotes and
733
   *                         which document type to use. The default is ENT_COMPAT.
734
   *                         <table>
735
   *                         Available <i>flags</i> constants
736
   *                         <tr valign="top">
737
   *                         <td>Constant Name</td>
738
   *                         <td>Description</td>
739
   *                         </tr>
740
   *                         <tr valign="top">
741
   *                         <td><b>ENT_COMPAT</b></td>
742
   *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
743
   *                         </tr>
744
   *                         <tr valign="top">
745
   *                         <td><b>ENT_QUOTES</b></td>
746
   *                         <td>Will convert both double and single quotes.</td>
747
   *                         </tr>
748
   *                         <tr valign="top">
749
   *                         <td><b>ENT_NOQUOTES</b></td>
750
   *                         <td>Will leave both double and single quotes unconverted.</td>
751
   *                         </tr>
752
   *                         <tr valign="top">
753
   *                         <td><b>ENT_HTML401</b></td>
754
   *                         <td>
755
   *                         Handle code as HTML 4.01.
756
   *                         </td>
757
   *                         </tr>
758
   *                         <tr valign="top">
759
   *                         <td><b>ENT_XML1</b></td>
760
   *                         <td>
761
   *                         Handle code as XML 1.
762
   *                         </td>
763
   *                         </tr>
764
   *                         <tr valign="top">
765
   *                         <td><b>ENT_XHTML</b></td>
766
   *                         <td>
767
   *                         Handle code as XHTML.
768
   *                         </td>
769
   *                         </tr>
770
   *                         <tr valign="top">
771
   *                         <td><b>ENT_HTML5</b></td>
772
   *                         <td>
773
   *                         Handle code as HTML 5.
774
   *                         </td>
775
   *                         </tr>
776
   *                         </table>
777
   *                         </p>
778
   *
779
   * @return static <p>Object with the resulting $str after being html decoded.</p>
780
   */
781 5
  public function htmlDecode(int $flags = ENT_COMPAT): self
782
  {
783 5
    $str = UTF8::html_entity_decode($this->str, $flags, $this->encoding);
784
785 5
    return static::create($str, $this->encoding);
786
  }
787
788
  /**
789
   * Convert all applicable characters to HTML entities.
790
   *
791
   * @param int $flags       [optional] <p>
792
   *                         A bitmask of one or more of the following flags, which specify how to handle quotes and
793
   *                         which document type to use. The default is ENT_COMPAT.
794
   *                         <table>
795
   *                         Available <i>flags</i> constants
796
   *                         <tr valign="top">
797
   *                         <td>Constant Name</td>
798
   *                         <td>Description</td>
799
   *                         </tr>
800
   *                         <tr valign="top">
801
   *                         <td><b>ENT_COMPAT</b></td>
802
   *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
803
   *                         </tr>
804
   *                         <tr valign="top">
805
   *                         <td><b>ENT_QUOTES</b></td>
806
   *                         <td>Will convert both double and single quotes.</td>
807
   *                         </tr>
808
   *                         <tr valign="top">
809
   *                         <td><b>ENT_NOQUOTES</b></td>
810
   *                         <td>Will leave both double and single quotes unconverted.</td>
811
   *                         </tr>
812
   *                         <tr valign="top">
813
   *                         <td><b>ENT_HTML401</b></td>
814
   *                         <td>
815
   *                         Handle code as HTML 4.01.
816
   *                         </td>
817
   *                         </tr>
818
   *                         <tr valign="top">
819
   *                         <td><b>ENT_XML1</b></td>
820
   *                         <td>
821
   *                         Handle code as XML 1.
822
   *                         </td>
823
   *                         </tr>
824
   *                         <tr valign="top">
825
   *                         <td><b>ENT_XHTML</b></td>
826
   *                         <td>
827
   *                         Handle code as XHTML.
828
   *                         </td>
829
   *                         </tr>
830
   *                         <tr valign="top">
831
   *                         <td><b>ENT_HTML5</b></td>
832
   *                         <td>
833
   *                         Handle code as HTML 5.
834
   *                         </td>
835
   *                         </tr>
836
   *                         </table>
837
   *                         </p>
838
   *
839
   * @return static <p>Object with the resulting $str after being html encoded.</p>
840
   */
841 5
  public function htmlEncode(int $flags = ENT_COMPAT): self
842
  {
843 5
    $str = UTF8::htmlentities($this->str, $flags, $this->encoding);
844
845 5
    return static::create($str, $this->encoding);
846
  }
847
848
  /**
849
   * Capitalizes the first word of the string, replaces underscores with
850
   * spaces, and strips '_id'.
851
   *
852
   * @return static <p>Object with a humanized $str.</p>
853
   */
854 3
  public function humanize(): self
855
  {
856 3
    $str = UTF8::str_replace(['_id', '_'], ['', ' '], $this->str);
857
858 3
    return static::create($str, $this->encoding)->trim()->upperCaseFirst();
859
  }
860
861
  /**
862
   * Converts the first character of the supplied string to upper case.
863
   *
864
   * @return static <p>Object with the first character of $str being upper case.</p>
865
   */
866 61 View Code Duplication
  public function upperCaseFirst(): self
0 ignored issues
show
Duplication introduced by
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...
867
  {
868 61
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
869 61
    $rest = UTF8::substr(
870 61
        $this->str,
871 61
        1,
872 61
        $this->length() - 1,
873 61
        $this->encoding
874
    );
875
876 61
    $str = UTF8::strtoupper($first, $this->encoding) . $rest;
0 ignored issues
show
Security Bug introduced by
It seems like $first defined by \voku\helper\UTF8::subst... 0, 1, $this->encoding) on line 868 can also be of type false; however, voku\helper\UTF8::strtoupper() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
877
878 61
    return static::create($str, $this->encoding);
879
  }
880
881
  /**
882
   * Returns the index of the last occurrence of $needle in the string,
883
   * and false if not found. Accepts an optional offset from which to begin
884
   * the search. Offsets may be negative to count from the last character
885
   * in the string.
886
   *
887
   * @param  string $needle <p>Substring to look for.</p>
888
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
889
   *
890
   * @return int|false <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
891
   */
892 12
  public function indexOfLast(string $needle, int $offset = 0)
893
  {
894 12
    return UTF8::strrpos($this->str, $needle, $offset, $this->encoding);
895
  }
896
897
  /**
898
   * Returns the index of the last occurrence of $needle in the string,
899
   * and false if not found. Accepts an optional offset from which to begin
900
   * the search. Offsets may be negative to count from the last character
901
   * in the string.
902
   *
903
   * @param  string $needle <p>Substring to look for.</p>
904
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
905
   *
906
   * @return int|false <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
907
   */
908 2
  public function indexOfLastIgnoreCase(string $needle, int $offset = 0)
909
  {
910 2
    return UTF8::strripos($this->str, $needle, $offset, $this->encoding);
911
  }
912
913
  /**
914
   * Inserts $substring into the string at the $index provided.
915
   *
916
   * @param  string $substring <p>String to be inserted.</p>
917
   * @param  int    $index     <p>The index at which to insert the substring.</p>
918
   *
919
   * @return static <p>Object with the resulting $str after the insertion.</p>
920
   */
921 8 View Code Duplication
  public function insert(string $substring, int $index): self
0 ignored issues
show
Duplication introduced by
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...
922
  {
923 8
    $stringy = static::create($this->str, $this->encoding);
924 8
    if ($index > $stringy->length()) {
925 1
      return $stringy;
926
    }
927
928 7
    $start = UTF8::substr($stringy->str, 0, $index, $stringy->encoding);
929 7
    $end = UTF8::substr($stringy->str, $index, $stringy->length(), $stringy->encoding);
930
931 7
    $stringy->str = $start . $substring . $end;
932
933 7
    return $stringy;
934
  }
935
936
  /**
937
   * Returns true if the string contains the $pattern, otherwise false.
938
   *
939
   * WARNING: Asterisks ("*") are translated into (".*") zero-or-more regular
940
   * expression wildcards.
941
   *
942
   * @credit Originally from Laravel, thanks Taylor.
943
   *
944
   * @param string $pattern <p>The string or pattern to match against.</p>
945
   *
946
   * @return bool <p>Whether or not we match the provided pattern.</p>
947
   */
948 13
  public function is(string $pattern): bool
949
  {
950 13
    if ($this->toString() === $pattern) {
951 1
      return true;
952
    }
953
954 12
    $quotedPattern = \preg_quote($pattern, '/');
955 12
    $replaceWildCards = \str_replace('\*', '.*', $quotedPattern);
956
957 12
    return $this->matchesPattern('^' . $replaceWildCards . '\z');
958
  }
959
960
  /**
961
   * Returns true if the string contains only alphabetic chars, false otherwise.
962
   *
963
   * @return bool <p>Whether or not $str contains only alphabetic chars.</p>
964
   */
965 10
  public function isAlpha(): bool
966
  {
967 10
    return $this->matchesPattern('^[[:alpha:]]*$');
968
  }
969
970
  /**
971
   * Determine whether the string is considered to be empty.
972
   *
973
   * A variable is considered empty if it does not exist or if its value equals FALSE.
974
   * empty() does not generate a warning if the variable does not exist.
975
   *
976
   * @return bool <p>Whether or not $str is empty().</p>
977
   */
978
  public function isEmpty(): bool
979
  {
980
    return empty($this->str);
981
  }
982
983
  /**
984
   * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
985
   *
986
   * @return bool <p>Whether or not $str contains only alphanumeric chars.</p>
987
   */
988 13
  public function isAlphanumeric(): bool
989
  {
990 13
    return $this->matchesPattern('^[[:alnum:]]*$');
991
  }
992
993
  /**
994
   * Returns true if the string contains only whitespace chars, false otherwise.
995
   *
996
   * @return bool <p>Whether or not $str contains only whitespace characters.</p>
997
   */
998 15
  public function isBlank(): bool
999
  {
1000 15
    return $this->matchesPattern('^[[:space:]]*$');
1001
  }
1002
1003
  /**
1004
   * Returns true if the string contains only hexadecimal chars, false otherwise.
1005
   *
1006
   * @return bool <p>Whether or not $str contains only hexadecimal chars.</p>
1007
   */
1008 13
  public function isHexadecimal(): bool
1009
  {
1010 13
    return $this->matchesPattern('^[[:xdigit:]]*$');
1011
  }
1012
1013
  /**
1014
   * Returns true if the string contains HTML-Tags, false otherwise.
1015
   *
1016
   * @return bool <p>Whether or not $str contains HTML-Tags.</p>
1017
   */
1018 1
  public function isHtml(): bool
1019
  {
1020 1
    return UTF8::is_html($this->str);
1021
  }
1022
1023
  /**
1024
   * Returns true if the string contains a valid E-Mail address, false otherwise.
1025
   *
1026
   * @param bool $useExampleDomainCheck   [optional] <p>Default: false</p>
1027
   * @param bool $useTypoInDomainCheck    [optional] <p>Default: false</p>
1028
   * @param bool $useTemporaryDomainCheck [optional] <p>Default: false</p>
1029
   * @param bool $useDnsCheck             [optional] <p>Default: false</p>
1030
   *
1031
   * @return bool <p>Whether or not $str contains a valid E-Mail address.</p>
1032
   */
1033 1
  public function isEmail(bool $useExampleDomainCheck = false, bool $useTypoInDomainCheck = false, bool $useTemporaryDomainCheck = false, bool $useDnsCheck = false): bool
1034
  {
1035 1
    return EmailCheck::isValid($this->str, $useExampleDomainCheck, $useTypoInDomainCheck, $useTemporaryDomainCheck, $useDnsCheck);
1036
  }
1037
1038
  /**
1039
   * Returns true if the string is JSON, false otherwise. Unlike json_decode
1040
   * in PHP 5.x, this method is consistent with PHP 7 and other JSON parsers,
1041
   * in that an empty string is not considered valid JSON.
1042
   *
1043
   * @return bool <p>Whether or not $str is JSON.</p>
1044
   */
1045 20
  public function isJson(): bool
1046
  {
1047 20
    if (!isset($this->str[0])) {
1048 1
      return false;
1049
    }
1050
1051 19
    \json_decode($this->str);
1052
1053 19
    return \json_last_error() === JSON_ERROR_NONE;
1054
  }
1055
1056
  /**
1057
   * Returns true if the string contains only lower case chars, false otherwise.
1058
   *
1059
   * @return bool <p>Whether or not $str contains only lower case characters.</p>
1060
   */
1061 8
  public function isLowerCase(): bool
1062
  {
1063 8
    if ($this->matchesPattern('^[[:lower:]]*$')) {
1064 3
      return true;
1065
    }
1066
1067 5
    return false;
1068
  }
1069
1070
  /**
1071
   * Returns true if the string is serialized, false otherwise.
1072
   *
1073
   * @return bool <p>Whether or not $str is serialized.</p>
1074
   */
1075 7
  public function isSerialized(): bool
1076
  {
1077 7
    if (!isset($this->str[0])) {
1078 1
      return false;
1079
    }
1080
1081
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1082
    /** @noinspection UnserializeExploitsInspection */
1083 6
    return $this->str === 'b:0;'
1084
           ||
1085 6
           @\unserialize($this->str) !== false;
1086
  }
1087
1088
  /**
1089
   * Returns true if the string contains only lower case chars, false
1090
   * otherwise.
1091
   *
1092
   * @return bool <p>Whether or not $str contains only lower case characters.</p>
1093
   */
1094 8
  public function isUpperCase(): bool
1095
  {
1096 8
    return $this->matchesPattern('^[[:upper:]]*$');
1097
  }
1098
1099
  /**
1100
   * Returns the last $n characters of the string.
1101
   *
1102
   * @param int $n <p>Number of characters to retrieve from the end.</p>
1103
   *
1104
   * @return static <p>Object with its $str being the last $n chars.</p>
1105
   */
1106 12 View Code Duplication
  public function last(int $n): self
0 ignored issues
show
Duplication introduced by
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...
1107
  {
1108 12
    $stringy = static::create($this->str, $this->encoding);
1109
1110 12
    if ($n <= 0) {
1111 4
      $stringy->str = '';
1112
    } else {
1113 8
      return $stringy->substr(-$n);
1114
    }
1115
1116 4
    return $stringy;
1117
  }
1118
1119
  /**
1120
   * Splits on newlines and carriage returns, returning an array of Stringy
1121
   * objects corresponding to the lines in the string.
1122
   *
1123
   * @return static[] <p>An array of Stringy objects.</p>
1124
   */
1125 15
  public function lines(): array
1126
  {
1127 15
    $array = \preg_split('/[\r\n]{1,2}/u', $this->str);
1128
    /** @noinspection CallableInLoopTerminationConditionInspection */
1129
    /** @noinspection ForeachInvariantsInspection */
1130 15 View Code Duplication
    for ($i = 0; $i < \count($array); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Duplication introduced by
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...
1131 15
      $array[$i] = static::create($array[$i], $this->encoding);
1132
    }
1133
1134 15
    return $array;
1135
  }
1136
1137
  /**
1138
   * Returns the longest common prefix between the string and $otherStr.
1139
   *
1140
   * @param string $otherStr <p>Second string for comparison.</p>
1141
   *
1142
   * @return static <p>Object with its $str being the longest common prefix.</p>
1143
   */
1144 10
  public function longestCommonPrefix(string $otherStr): self
1145
  {
1146 10
    $encoding = $this->encoding;
1147 10
    $maxLength = \min($this->length(), UTF8::strlen($otherStr, $encoding));
1148
1149 10
    $longestCommonPrefix = '';
1150 10
    for ($i = 0; $i < $maxLength; $i++) {
1151 8
      $char = UTF8::substr($this->str, $i, 1, $encoding);
1152
1153 8
      if ($char == UTF8::substr($otherStr, $i, 1, $encoding)) {
1154 6
        $longestCommonPrefix .= $char;
1155
      } else {
1156 6
        break;
1157
      }
1158
    }
1159
1160 10
    return static::create($longestCommonPrefix, $encoding);
1161
  }
1162
1163
  /**
1164
   * Returns the longest common suffix between the string and $otherStr.
1165
   *
1166
   * @param string $otherStr <p>Second string for comparison.</p>
1167
   *
1168
   * @return static <p>Object with its $str being the longest common suffix.</p>
1169
   */
1170 10
  public function longestCommonSuffix(string $otherStr): self
1171
  {
1172 10
    $encoding = $this->encoding;
1173 10
    $maxLength = \min($this->length(), UTF8::strlen($otherStr, $encoding));
1174
1175 10
    $longestCommonSuffix = '';
1176 10
    for ($i = 1; $i <= $maxLength; $i++) {
1177 8
      $char = UTF8::substr($this->str, -$i, 1, $encoding);
1178
1179 8
      if ($char == UTF8::substr($otherStr, -$i, 1, $encoding)) {
1180 6
        $longestCommonSuffix = $char . $longestCommonSuffix;
1181
      } else {
1182 6
        break;
1183
      }
1184
    }
1185
1186 10
    return static::create($longestCommonSuffix, $encoding);
1187
  }
1188
1189
  /**
1190
   * Returns the longest common substring between the string and $otherStr.
1191
   * In the case of ties, it returns that which occurs first.
1192
   *
1193
   * @param string $otherStr <p>Second string for comparison.</p>
1194
   *
1195
   * @return static <p>Object with its $str being the longest common substring.</p>
1196
   */
1197 10
  public function longestCommonSubstring(string $otherStr): self
1198
  {
1199
    // Uses dynamic programming to solve
1200
    // http://en.wikipedia.org/wiki/Longest_common_substring_problem
1201 10
    $encoding = $this->encoding;
1202 10
    $stringy = static::create($this->str, $encoding);
1203 10
    $strLength = $stringy->length();
1204 10
    $otherLength = UTF8::strlen($otherStr, $encoding);
1205
1206
    // Return if either string is empty
1207 10
    if ($strLength == 0 || $otherLength == 0) {
1208 2
      $stringy->str = '';
1209
1210 2
      return $stringy;
1211
    }
1212
1213 8
    $len = 0;
1214 8
    $end = 0;
1215 8
    $table = \array_fill(
1216 8
        0,
1217 8
        $strLength + 1,
1218 8
        \array_fill(0, $otherLength + 1, 0)
1219
    );
1220
1221 8
    for ($i = 1; $i <= $strLength; $i++) {
1222 8
      for ($j = 1; $j <= $otherLength; $j++) {
1223 8
        $strChar = UTF8::substr($stringy->str, $i - 1, 1, $encoding);
1224 8
        $otherChar = UTF8::substr($otherStr, $j - 1, 1, $encoding);
1225
1226 8
        if ($strChar == $otherChar) {
1227 8
          $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
1228 8
          if ($table[$i][$j] > $len) {
1229 8
            $len = $table[$i][$j];
1230 8
            $end = $i;
1231
          }
1232
        } else {
1233 8
          $table[$i][$j] = 0;
1234
        }
1235
      }
1236
    }
1237
1238 8
    $stringy->str = UTF8::substr($stringy->str, $end - $len, $len, $encoding);
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\helper\UTF8::subst... $len, $len, $encoding) can also be of type false. However, the property $str is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1239
1240 8
    return $stringy;
1241
  }
1242
1243
  /**
1244
   * Returns whether or not a character exists at an index. Offsets may be
1245
   * negative to count from the last character in the string. Implements
1246
   * part of the ArrayAccess interface.
1247
   *
1248
   * @param int $offset <p>The index to check.</p>
1249
   *
1250
   * @return boolean <p>Whether or not the index exists.</p>
1251
   */
1252 6
  public function offsetExists($offset): bool
1253
  {
1254
    // init
1255 6
    $length = $this->length();
1256 6
    $offset = (int)$offset;
1257
1258 6
    if ($offset >= 0) {
1259 3
      return ($length > $offset);
1260
    }
1261
1262 3
    return ($length >= \abs($offset));
1263
  }
1264
1265
  /**
1266
   * Returns the character at the given index. Offsets may be negative to
1267
   * count from the last character in the string. Implements part of the
1268
   * ArrayAccess interface, and throws an OutOfBoundsException if the index
1269
   * does not exist.
1270
   *
1271
   * @param int $offset <p>The <strong>index</strong> from which to retrieve the char.</p>
1272
   *
1273
   * @return string <p>The character at the specified index.</p>
1274
   *
1275
   * @throws \OutOfBoundsException <p>If the positive or negative offset does not exist.</p>
1276
   */
1277 2
  public function offsetGet($offset): string
1278
  {
1279
    // init
1280 2
    $offset = (int)$offset;
1281 2
    $length = $this->length();
1282
1283
    if (
1284 2
        ($offset >= 0 && $length <= $offset)
1285
        ||
1286 2
        $length < \abs($offset)
1287
    ) {
1288 1
      throw new \OutOfBoundsException('No character exists at the index');
1289
    }
1290
1291 1
    return UTF8::substr($this->str, $offset, 1, $this->encoding);
1292
  }
1293
1294
  /**
1295
   * Implements part of the ArrayAccess interface, but throws an exception
1296
   * when called. This maintains the immutability of Stringy objects.
1297
   *
1298
   * @param int   $offset <p>The index of the character.</p>
1299
   * @param mixed $value  <p>Value to set.</p>
1300
   *
1301
   * @throws \Exception <p>When called.</p>
1302
   */
1303 1
  public function offsetSet($offset, $value)
1304
  {
1305
    // Stringy is immutable, cannot directly set char
1306
    /** @noinspection ThrowRawExceptionInspection */
1307 1
    throw new \Exception('Stringy object is immutable, cannot modify char');
1308
  }
1309
1310
  /**
1311
   * Implements part of the ArrayAccess interface, but throws an exception
1312
   * when called. This maintains the immutability of Stringy objects.
1313
   *
1314
   * @param int $offset <p>The index of the character.</p>
1315
   *
1316
   * @throws \Exception <p>When called.</p>
1317
   */
1318 1
  public function offsetUnset($offset)
1319
  {
1320
    // Don't allow directly modifying the string
1321
    /** @noinspection ThrowRawExceptionInspection */
1322 1
    throw new \Exception('Stringy object is immutable, cannot unset char');
1323
  }
1324
1325
  /**
1326
   * Pads the string to a given length with $padStr. If length is less than
1327
   * or equal to the length of the string, no padding takes places. The
1328
   * default string used for padding is a space, and the default type (one of
1329
   * 'left', 'right', 'both') is 'right'. Throws an InvalidArgumentException
1330
   * if $padType isn't one of those 3 values.
1331
   *
1332
   * @param int    $length  <p>Desired string length after padding.</p>
1333
   * @param string $padStr  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1334
   * @param string $padType [optional] <p>One of 'left', 'right', 'both'. Default: 'right'</p>
1335
   *
1336
   * @return static <p>Object with a padded $str.</p>
1337
   *
1338
   * @throws \InvalidArgumentException <p>If $padType isn't one of 'right', 'left' or 'both'.</p>
1339
   */
1340 13
  public function pad(int $length, string $padStr = ' ', string $padType = 'right'): self
1341
  {
1342 13
    if (!\in_array($padType, ['left', 'right', 'both'], true)) {
1343 1
      throw new \InvalidArgumentException(
1344 1
          'Pad expects $padType ' . "to be one of 'left', 'right' or 'both'"
1345
      );
1346
    }
1347
1348
    switch ($padType) {
1349 12
      case 'left':
1350 3
        return $this->padLeft($length, $padStr);
1351 9
      case 'right':
1352 6
        return $this->padRight($length, $padStr);
1353
      default:
1354 3
        return $this->padBoth($length, $padStr);
1355
    }
1356
  }
1357
1358
  /**
1359
   * Returns a new string of a given length such that the beginning of the
1360
   * string is padded. Alias for pad() with a $padType of 'left'.
1361
   *
1362
   * @param int    $length <p>Desired string length after padding.</p>
1363
   * @param string $padStr [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1364
   *
1365
   * @return static <p>String with left padding.</p>
1366
   */
1367 10
  public function padLeft(int $length, string $padStr = ' '): self
1368
  {
1369 10
    return $this->applyPadding($length - $this->length(), 0, $padStr);
1370
  }
1371
1372
  /**
1373
   * Adds the specified amount of left and right padding to the given string.
1374
   * The default character used is a space.
1375
   *
1376
   * @param int    $left   [optional] <p>Length of left padding. Default: 0</p>
1377
   * @param int    $right  [optional] <p>Length of right padding. Default: 0</p>
1378
   * @param string $padStr [optional] <p>String used to pad. Default: ' '</p>
1379
   *
1380
   * @return static <p>String with padding applied.</p>
1381
   */
1382 37
  protected function applyPadding(int $left = 0, int $right = 0, string $padStr = ' '): self
1383
  {
1384 37
    $stringy = static::create($this->str, $this->encoding);
1385
1386 37
    $length = UTF8::strlen($padStr, $stringy->encoding);
1387
1388 37
    $strLength = $stringy->length();
1389 37
    $paddedLength = $strLength + $left + $right;
1390
1391 37
    if (!$length || $paddedLength <= $strLength) {
1392 3
      return $stringy;
1393
    }
1394
1395 34
    $leftPadding = UTF8::substr(
1396 34
        UTF8::str_repeat(
1397 34
            $padStr,
1398 34
            (int)\ceil($left / $length)
1399
        ),
1400 34
        0,
1401 34
        $left,
1402 34
        $stringy->encoding
1403
    );
1404
1405 34
    $rightPadding = UTF8::substr(
1406 34
        UTF8::str_repeat(
1407 34
            $padStr,
1408 34
            (int)\ceil($right / $length)
1409
        ),
1410 34
        0,
1411 34
        $right,
1412 34
        $stringy->encoding
1413
    );
1414
1415 34
    $stringy->str = $leftPadding . $stringy->str . $rightPadding;
1416
1417 34
    return $stringy;
1418
  }
1419
1420
  /**
1421
   * Returns a new string of a given length such that the end of the string
1422
   * is padded. Alias for pad() with a $padType of 'right'.
1423
   *
1424
   * @param int    $length <p>Desired string length after padding.</p>
1425
   * @param string $padStr [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1426
   *
1427
   * @return static <p>String with right padding.</p>
1428
   */
1429 13
  public function padRight(int $length, string $padStr = ' '): self
1430
  {
1431 13
    return $this->applyPadding(0, $length - $this->length(), $padStr);
1432
  }
1433
1434
  /**
1435
   * Returns a new string of a given length such that both sides of the
1436
   * string are padded. Alias for pad() with a $padType of 'both'.
1437
   *
1438
   * @param int    $length <p>Desired string length after padding.</p>
1439
   * @param string $padStr [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1440
   *
1441
   * @return static <p>String with padding applied.</p>
1442
   */
1443 14
  public function padBoth(int $length, string $padStr = ' '): self
1444
  {
1445 14
    $padding = $length - $this->length();
1446
1447 14
    return $this->applyPadding((int)\floor($padding / 2), (int)\ceil($padding / 2), $padStr);
1448
  }
1449
1450
  /**
1451
   * Returns a new string starting with $string.
1452
   *
1453
   * @param string $string <p>The string to append.</p>
1454
   *
1455
   * @return static <p>Object with appended $string.</p>
1456
   */
1457 2
  public function prepend(string $string): self
1458
  {
1459 2
    return static::create($string . $this->str, $this->encoding);
1460
  }
1461
1462
  /**
1463
   * Returns a new string with the prefix $substring removed, if present.
1464
   *
1465
   * @param string $substring <p>The prefix to remove.</p>
1466
   *
1467
   * @return static <p>Object having a $str without the prefix $substring.</p>
1468
   */
1469 12 View Code Duplication
  public function removeLeft(string $substring): self
0 ignored issues
show
Duplication introduced by
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...
1470
  {
1471 12
    $stringy = static::create($this->str, $this->encoding);
1472
1473 12
    if ($stringy->startsWith($substring)) {
1474 6
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1475
1476 6
      return $stringy->substr($substringLength);
1477
    }
1478
1479 6
    return $stringy;
1480
  }
1481
1482
  /**
1483
   * Returns a new string with the suffix $substring removed, if present.
1484
   *
1485
   * @param string $substring <p>The suffix to remove.</p>
1486
   *
1487
   * @return static <p>Object having a $str without the suffix $substring.</p>
1488
   */
1489 12 View Code Duplication
  public function removeRight(string $substring): self
0 ignored issues
show
Duplication introduced by
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...
1490
  {
1491 12
    $stringy = static::create($this->str, $this->encoding);
1492
1493 12
    if ($stringy->endsWith($substring)) {
1494 8
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1495
1496 8
      return $stringy->substr(0, $stringy->length() - $substringLength);
1497
    }
1498
1499 4
    return $stringy;
1500
  }
1501
1502
  /**
1503
   * Returns a repeated string given a multiplier.
1504
   *
1505
   * @param int $multiplier <p>The number of times to repeat the string.</p>
1506
   *
1507
   * @return static <p>Object with a repeated str.</p>
1508
   */
1509 7
  public function repeat(int $multiplier): self
1510
  {
1511 7
    $repeated = UTF8::str_repeat($this->str, $multiplier);
1512
1513 7
    return static::create($repeated, $this->encoding);
1514
  }
1515
1516
  /**
1517
   * Replaces all occurrences of $search in $str by $replacement.
1518
   *
1519
   * @param string $search        <p>The needle to search for.</p>
1520
   * @param string $replacement   <p>The string to replace with.</p>
1521
   * @param bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
1522
   *
1523
   * @return static <p>Object with the resulting $str after the replacements.</p>
1524
   */
1525 29 View Code Duplication
  public function replace(string $search, string $replacement, bool $caseSensitive = true): self
0 ignored issues
show
Duplication introduced by
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...
1526
  {
1527 29
    if ($caseSensitive) {
1528 22
      $return = UTF8::str_replace($search, $replacement, $this->str);
1529
    } else {
1530 7
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1531
    }
1532
1533 29
    return static::create($return);
1534
  }
1535
1536
  /**
1537
   * Replaces all occurrences of $search in $str by $replacement.
1538
   *
1539
   * @param array        $search        <p>The elements to search for.</p>
1540
   * @param string|array $replacement   <p>The string to replace with.</p>
1541
   * @param bool         $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
1542
   *
1543
   * @return static <p>Object with the resulting $str after the replacements.</p>
1544
   */
1545 30 View Code Duplication
  public function replaceAll(array $search, $replacement, bool $caseSensitive = true): self
0 ignored issues
show
Duplication introduced by
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...
1546
  {
1547 30
    if ($caseSensitive) {
1548 23
      $return = UTF8::str_replace($search, $replacement, $this->str);
1549
    } else {
1550 7
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1551
    }
1552
1553 30
    return static::create($return);
1554
  }
1555
1556
  /**
1557
   * Replaces all occurrences of $search from the beginning of string with $replacement.
1558
   *
1559
   * @param string $search      <p>The string to search for.</p>
1560
   * @param string $replacement <p>The replacement.</p>
1561
   *
1562
   * @return static <p>Object with the resulting $str after the replacements.</p>
1563
   */
1564 16
  public function replaceBeginning(string $search, string $replacement): self
1565
  {
1566 16
    $str = $this->regexReplace('^' . \preg_quote($search, '/'), UTF8::str_replace('\\', '\\\\', $replacement));
1567
1568 16
    return static::create($str, $this->encoding);
1569
  }
1570
1571
  /**
1572
   * Replaces all occurrences of $search from the ending of string with $replacement.
1573
   *
1574
   * @param string $search      <p>The string to search for.</p>
1575
   * @param string $replacement <p>The replacement.</p>
1576
   *
1577
   * @return static <p>Object with the resulting $str after the replacements.</p>
1578
   */
1579 16
  public function replaceEnding(string $search, string $replacement): self
1580
  {
1581 16
    $str = $this->regexReplace(\preg_quote($search, '/') . '$', UTF8::str_replace('\\', '\\\\', $replacement));
1582
1583 16
    return static::create($str, $this->encoding);
1584
  }
1585
1586
  /**
1587
   * Gets the substring after (or before via "$beforeNeedle") the first occurrence of the "$needle".
1588
   * If no match is found returns new empty Stringy object.
1589
   *
1590
   * @param string $needle       <p>The string to look for.</p>
1591
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1592
   *
1593
   * @return static
1594
   */
1595 2 View Code Duplication
  public function substringOf(string $needle, bool $beforeNeedle = false): self
0 ignored issues
show
Duplication introduced by
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...
1596
  {
1597 2
    if ('' === $needle) {
1598
      return static::create();
1599
    }
1600
1601 2
    if (false === $part = UTF8::strstr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1602 2
      return static::create();
1603
    }
1604
1605 2
    return static::create($part);
1606
  }
1607
1608
  /**
1609
   * Gets the substring after (or before via "$beforeNeedle") the first occurrence of the "$needle".
1610
   * If no match is found returns new empty Stringy object.
1611
   *
1612
   * @param string $needle       <p>The string to look for.</p>
1613
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1614
   *
1615
   * @return static
1616
   */
1617 2 View Code Duplication
  public function substringOfIgnoreCase(string $needle, bool $beforeNeedle = false): self
0 ignored issues
show
Duplication introduced by
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...
1618
  {
1619 2
    if ('' === $needle) {
1620
      return static::create();
1621
    }
1622
1623 2
    if (false === $part = UTF8::stristr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1624 2
      return static::create();
1625
    }
1626
1627 2
    return static::create($part);
1628
  }
1629
1630
  /**
1631
   * Gets the substring after (or before via "$beforeNeedle") the last occurrence of the "$needle".
1632
   * If no match is found returns new empty Stringy object.
1633
   *
1634
   * @param string $needle       <p>The string to look for.</p>
1635
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1636
   *
1637
   * @return static
1638
   */
1639 2 View Code Duplication
  public function lastSubstringOf(string $needle, bool $beforeNeedle = false): self
0 ignored issues
show
Duplication introduced by
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...
1640
  {
1641 2
    if ('' === $needle) {
1642
      return static::create();
1643
    }
1644
1645 2
    if (false === $part = UTF8::strrchr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1646 2
      return static::create();
1647
    }
1648
1649 2
    return static::create($part);
1650
  }
1651
1652
  /**
1653
   * Gets the substring after (or before via "$beforeNeedle") the last occurrence of the "$needle".
1654
   * If no match is found returns new empty Stringy object.
1655
   *
1656
   * @param string $needle       <p>The string to look for.</p>
1657
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1658
   *
1659
   * @return static
1660
   */
1661 1 View Code Duplication
  public function lastSubstringOfIgnoreCase(string $needle, bool $beforeNeedle = false): self
0 ignored issues
show
Duplication introduced by
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...
1662
  {
1663 1
    if ('' === $needle) {
1664
      return static::create();
1665
    }
1666
1667 1
    if (false === $part = UTF8::strrichr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1668 1
      return static::create();
1669
    }
1670
1671 1
    return static::create($part);
1672
  }
1673
1674
  /**
1675
   * Returns a reversed string. A multibyte version of strrev().
1676
   *
1677
   * @return static <p>Object with a reversed $str.</p>
1678
   */
1679 5
  public function reverse(): self
1680
  {
1681 5
    $reversed = UTF8::strrev($this->str);
1682
1683 5
    return static::create($reversed, $this->encoding);
1684
  }
1685
1686
  /**
1687
   * Truncates the string to a given length, while ensuring that it does not
1688
   * split words. If $substring is provided, and truncating occurs, the
1689
   * string is further truncated so that the substring may be appended without
1690
   * exceeding the desired length.
1691
   *
1692
   * @param int    $length    <p>Desired length of the truncated string.</p>
1693
   * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
1694
   *
1695
   * @return static <p>Object with the resulting $str after truncating.</p>
1696
   */
1697 23
  public function safeTruncate(int $length, string $substring = ''): self
1698
  {
1699 23
    $stringy = static::create($this->str, $this->encoding);
1700 23
    if ($length >= $stringy->length()) {
1701 4
      return $stringy;
1702
    }
1703
1704
    // need to further trim the string so we can append the substring
1705 19
    $encoding = $stringy->encoding;
1706 19
    $substringLength = UTF8::strlen($substring, $encoding);
1707 19
    $length -= $substringLength;
1708
1709 19
    $truncated = UTF8::substr($stringy->str, 0, $length, $encoding);
1710
1711
    // if the last word was truncated
1712 19
    $strPosSpace = UTF8::strpos($stringy->str, ' ', $length - 1, $encoding);
1713 19
    if ($strPosSpace != $length) {
1714
      // find pos of the last occurrence of a space, get up to that
1715 12
      $lastPos = UTF8::strrpos($truncated, ' ', 0, $encoding);
0 ignored issues
show
Security Bug introduced by
It seems like $truncated defined by \voku\helper\UTF8::subst... 0, $length, $encoding) on line 1709 can also be of type false; however, voku\helper\UTF8::strrpos() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1716
1717 12
      if ($lastPos !== false || $strPosSpace !== false) {
1718 11
        $truncated = UTF8::substr($truncated, 0, (int)$lastPos, $encoding);
0 ignored issues
show
Security Bug introduced by
It seems like $truncated defined by \voku\helper\UTF8::subst...t) $lastPos, $encoding) on line 1718 can also be of type false; however, voku\helper\UTF8::substr() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1719
      }
1720
    }
1721
1722 19
    $stringy->str = $truncated . $substring;
1723
1724 19
    return $stringy;
1725
  }
1726
1727
  /**
1728
   * A multibyte string shuffle function. It returns a string with its
1729
   * characters in random order.
1730
   *
1731
   * @return static <p>Object with a shuffled $str.</p>
1732
   */
1733 3
  public function shuffle(): self
1734
  {
1735 3
    $shuffledStr = UTF8::str_shuffle($this->str);
1736
1737 3
    return static::create($shuffledStr, $this->encoding);
1738
  }
1739
1740
  /**
1741
   * Converts the string into an URL slug. This includes replacing non-ASCII
1742
   * characters with their closest ASCII equivalents, removing remaining
1743
   * non-ASCII and non-alphanumeric characters, and replacing whitespace with
1744
   * $replacement. The replacement defaults to a single dash, and the string
1745
   * is also converted to lowercase.
1746
   *
1747
   * @param string $replacement [optional] <p>The string used to replace whitespace. Default: '-'</p>
1748
   * @param string $language    [optional] <p>The language for the url. Default: 'de'</p>
1749
   * @param bool   $strToLower  [optional] <p>string to lower. Default: true</p>
1750
   *
1751
   * @return static <p>Object whose $str has been converted to an URL slug.</p>
1752
   */
1753 15
  public function slugify(string $replacement = '-', string $language = 'de', bool $strToLower = true): self
1754
  {
1755 15
    $slug = URLify::slug($this->str, $language, $replacement, $strToLower);
1756
1757 15
    return static::create($slug, $this->encoding);
1758
  }
1759
1760
  /**
1761
   * Remove css media-queries.
1762
   *
1763
   * @return static
1764
   */
1765 1
  public function stripeCssMediaQueries(): self
1766
  {
1767 1
    $pattern = '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU';
1768
1769 1
    return static::create(\preg_replace($pattern, '', $this->str));
1770
  }
1771
1772
  /**
1773
   * Strip all whitespace characters. This includes tabs and newline characters,
1774
   * as well as multibyte whitespace such as the thin space and ideographic space.
1775
   *
1776
   * @return static
1777
   */
1778 12
  public function stripWhitespace(): self
1779
  {
1780 12
    return static::create(UTF8::strip_whitespace($this->str));
1781
  }
1782
1783
  /**
1784
   * Remove empty html-tag.
1785
   *
1786
   * e.g.: <tag></tag>
1787
   *
1788
   * @return static
1789
   */
1790 1
  public function stripeEmptyHtmlTags(): self
1791
  {
1792 1
    $pattern = "/<[^\/>]*>(([\s]?)*|)<\/[^>]*>/i";
1793
1794 1
    return static::create(\preg_replace($pattern, '', $this->str));
1795
  }
1796
1797
  /**
1798
   * Converts the string into an valid UTF-8 string.
1799
   *
1800
   * @return static
1801
   */
1802 1
  public function utf8ify(): self
1803
  {
1804 1
    return static::create(UTF8::cleanup($this->str));
1805
  }
1806
1807
  /**
1808
   * Create a escape html version of the string via "UTF8::htmlspecialchars()".
1809
   *
1810
   * @return static
1811
   */
1812 6
  public function escape(): self
1813
  {
1814 6
    $str = UTF8::htmlspecialchars(
1815 6
        $this->str,
1816 6
        ENT_QUOTES | ENT_SUBSTITUTE,
1817 6
        $this->encoding
1818
    );
1819
1820 6
    return static::create($str, $this->encoding);
1821
  }
1822
1823
  /**
1824
   * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1825
   *
1826
   * @param string   $search
1827
   * @param int|null $length                 [optional] <p>Default: null === text->length / 2</p>
1828
   * @param string   $replacerForSkippedText [optional] <p>Default: …</p>
1829
   *
1830
   * @return static
1831
   */
1832 1
  public function extractText(string $search = '', int $length = null, string $replacerForSkippedText = '…'): self
1833
  {
1834
    // init
1835 1
    $text = $this->str;
1836
1837 1
    if (empty($text)) {
1838 1
      return static::create('', $this->encoding);
1839
    }
1840
1841 1
    $trimChars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1842
1843 1
    if ($length === null) {
1844 1
      $length = (int)\round($this->length() / 2, 0);
1845
    }
1846
1847 1
    if (empty($search)) {
1848
1849 1
      $stringLength = UTF8::strlen($text, $this->encoding);
1850
1851 1
      if ($length > 0) {
1852 1
        $end = ($length - 1) > $stringLength ? $stringLength : ($length - 1);
1853
      } else {
1854 1
        $end = 0;
1855
      }
1856
1857 1
      $pos = \min(
1858 1
          UTF8::strpos($text, ' ', $end, $this->encoding),
1859 1
          UTF8::strpos($text, '.', $end, $this->encoding)
1860
      );
1861
1862 1
      if ($pos) {
1863 1
        return static::create(
1864 1
            \rtrim(
1865 1
                UTF8::substr($text, 0, $pos, $this->encoding),
1866 1
                $trimChars
1867 1
            ) . $replacerForSkippedText,
1868 1
            $this->encoding
1869
        );
1870
      }
1871
1872
      return static::create($text, $this->encoding);
1873
    }
1874
1875 1
    $wordPos = UTF8::stripos(
1876 1
        $text,
1877 1
        $search,
1878 1
        0,
1879 1
        $this->encoding
1880
    );
1881 1
    $halfSide = (int)($wordPos - $length / 2 + UTF8::strlen($search, $this->encoding) / 2);
1882
1883 1
    if ($halfSide > 0) {
1884
1885 1
      $halfText = UTF8::substr($text, 0, $halfSide, $this->encoding);
1886 1
      $pos_start = \max(UTF8::strrpos($halfText, ' ', 0), UTF8::strrpos($halfText, '.', 0));
0 ignored issues
show
Security Bug introduced by
It seems like $halfText defined by \voku\helper\UTF8::subst...fSide, $this->encoding) on line 1885 can also be of type false; however, voku\helper\UTF8::strrpos() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1887
1888 1
      if (!$pos_start) {
1889 1
        $pos_start = 0;
1890
      }
1891
1892
    } else {
1893 1
      $pos_start = 0;
1894
    }
1895
1896 1
    if ($wordPos && $halfSide > 0) {
1897 1
      $l = $pos_start + $length - 1;
1898 1
      $realLength = UTF8::strlen($text, $this->encoding);
1899
1900 1
      if ($l > $realLength) {
1901
        $l = $realLength;
1902
      }
1903
1904 1
      $pos_end = \min(
1905 1
                     UTF8::strpos($text, ' ', $l, $this->encoding),
1906 1
                     UTF8::strpos($text, '.', $l, $this->encoding)
1907 1
                 ) - $pos_start;
1908
1909 1
      if (!$pos_end || $pos_end <= 0) {
1910 1
        $extract = $replacerForSkippedText . \ltrim(
1911 1
                UTF8::substr(
1912 1
                    $text,
1913 1
                    $pos_start,
1914 1
                    UTF8::strlen($text),
1915 1
                    $this->encoding
1916
                ),
1917 1
                $trimChars
1918
            );
1919 View Code Duplication
      } else {
0 ignored issues
show
Duplication introduced by
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...
1920 1
        $extract = $replacerForSkippedText . \trim(
1921 1
                UTF8::substr(
1922 1
                    $text,
1923 1
                    $pos_start,
1924 1
                    $pos_end,
1925 1
                    $this->encoding
1926
                ),
1927 1
                $trimChars
1928 1
            ) . $replacerForSkippedText;
1929
      }
1930
1931
    } else {
1932
1933 1
      $l = $length - 1;
1934 1
      $trueLength = UTF8::strlen($text, $this->encoding);
1935
1936 1
      if ($l > $trueLength) {
1937
        $l = $trueLength;
1938
      }
1939
1940 1
      $pos_end = \min(
1941 1
          UTF8::strpos($text, ' ', $l, $this->encoding),
1942 1
          UTF8::strpos($text, '.', $l, $this->encoding)
1943
      );
1944
1945 1 View Code Duplication
      if ($pos_end) {
0 ignored issues
show
Duplication introduced by
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...
1946 1
        $extract = \rtrim(
1947 1
                       UTF8::substr($text, 0, $pos_end, $this->encoding),
1948 1
                       $trimChars
1949 1
                   ) . $replacerForSkippedText;
1950
      } else {
1951 1
        $extract = $text;
1952
      }
1953
    }
1954
1955 1
    return static::create($extract, $this->encoding);
1956
  }
1957
1958
1959
  /**
1960
   * Try to remove all XSS-attacks from the string.
1961
   *
1962
   * @return static
1963
   */
1964 6
  public function removeXss(): self
1965
  {
1966 6
    static $antiXss = null;
1967
1968 6
    if ($antiXss === null) {
1969 1
      $antiXss = new AntiXSS();
1970
    }
1971
1972 6
    $str = $antiXss->xss_clean($this->str);
1973
1974 6
    return static::create($str, $this->encoding);
1975
  }
1976
1977
  /**
1978
   * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
1979
   *
1980
   * @param string $replacement [optional] <p>Default is a empty string.</p>
1981
   *
1982
   * @return static
1983
   */
1984 6
  public function removeHtmlBreak(string $replacement = ''): self
1985
  {
1986 6
    $str = (string)\preg_replace('#/\r\n|\r|\n|<br.*/?>#isU', $replacement, $this->str);
1987
1988 6
    return static::create($str, $this->encoding);
1989
  }
1990
1991
  /**
1992
   * Remove html via "strip_tags()" from the string.
1993
   *
1994
   * @param string $allowableTags [optional] <p>You can use the optional second parameter to specify tags which should
1995
   *                              not be stripped. Default: null
1996
   *                              </p>
1997
   *
1998
   * @return static
1999
   */
2000 6
  public function removeHtml(string $allowableTags = null): self
2001
  {
2002 6
    $str = \strip_tags($this->str, $allowableTags);
2003
2004 6
    return static::create($str, $this->encoding);
2005
  }
2006
2007
  /**
2008
   * Returns the substring beginning at $start, and up to, but not including
2009
   * the index specified by $end. If $end is omitted, the function extracts
2010
   * the remaining string. If $end is negative, it is computed from the end
2011
   * of the string.
2012
   *
2013
   * @param int $start <p>Initial index from which to begin extraction.</p>
2014
   * @param int $end   [optional] <p>Index at which to end extraction. Default: null</p>
2015
   *
2016
   * @return static <p>Object with its $str being the extracted substring.</p>
2017
   */
2018 18
  public function slice(int $start, int $end = null): self
2019
  {
2020 18
    if ($end === null) {
2021 4
      $length = $this->length();
2022 14
    } elseif ($end >= 0 && $end <= $start) {
2023 4
      return static::create('', $this->encoding);
2024 10
    } elseif ($end < 0) {
2025 2
      $length = $this->length() + $end - $start;
2026
    } else {
2027 8
      $length = $end - $start;
2028
    }
2029
2030 14
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
2031
2032 14
    return static::create($str, $this->encoding);
2033
  }
2034
2035
  /**
2036
   * Splits the string with the provided regular expression, returning an
2037
   * array of Stringy objects. An optional integer $limit will truncate the
2038
   * results.
2039
   *
2040
   * @param string $pattern <p>The regex with which to split the string.</p>
2041
   * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
2042
   *
2043
   * @return static[] <p>An array of Stringy objects.</p>
2044
   */
2045 16
  public function split(string $pattern, int $limit = -1): array
2046
  {
2047 16
    if ($limit === 0) {
2048 2
      return [];
2049
    }
2050
2051
    // this->split errors when supplied an empty pattern in < PHP 5.4.13
2052
    // and current versions of HHVM (3.8 and below)
2053 14
    if ($pattern === '') {
2054 1
      return [static::create($this->str, $this->encoding)];
2055
    }
2056
2057
    // this->split returns the remaining unsplit string in the last index when
2058
    // supplying a limit
2059 13
    if ($limit > 0) {
2060 8
      $limit += 1;
2061
    } else {
2062 5
      $limit = -1;
2063
    }
2064
2065 13
    $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $this->str, $limit);
2066
2067 13
    if ($limit > 0 && \count($array) === $limit) {
2068 4
      \array_pop($array);
2069
    }
2070
2071
    /** @noinspection CallableInLoopTerminationConditionInspection */
2072
    /** @noinspection ForeachInvariantsInspection */
2073 13 View Code Duplication
    for ($i = 0; $i < \count($array); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Duplication introduced by
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...
2074 13
      $array[$i] = static::create($array[$i], $this->encoding);
2075
    }
2076
2077 13
    return $array;
2078
  }
2079
2080
  /**
2081
   * Surrounds $str with the given substring.
2082
   *
2083
   * @param string $substring <p>The substring to add to both sides.</P>
2084
   *
2085
   * @return static <p>Object whose $str had the substring both prepended and appended.</p>
2086
   */
2087 5
  public function surround(string $substring): self
2088
  {
2089 5
    $str = \implode('', [$substring, $this->str, $substring]);
2090
2091 5
    return static::create($str, $this->encoding);
2092
  }
2093
2094
  /**
2095
   * Returns a case swapped version of the string.
2096
   *
2097
   * @return static <p>Object whose $str has each character's case swapped.</P>
2098
   */
2099 5
  public function swapCase(): self
2100
  {
2101 5
    $stringy = static::create($this->str, $this->encoding);
2102
2103 5
    $stringy->str = UTF8::swapCase($stringy->str, $stringy->encoding);
2104
2105 5
    return $stringy;
2106
  }
2107
2108
  /**
2109
   * Returns a string with smart quotes, ellipsis characters, and dashes from
2110
   * Windows-1252 (commonly used in Word documents) replaced by their ASCII
2111
   * equivalents.
2112
   *
2113
   * @return static <p>Object whose $str has those characters removed.</p>
2114
   */
2115 4
  public function tidy(): self
2116
  {
2117 4
    $str = UTF8::normalize_msword($this->str);
2118
2119 4
    return static::create($str, $this->encoding);
2120
  }
2121
2122
  /**
2123
   * Returns a trimmed string in proper title case.
2124
   *
2125
   * Also accepts an array, $ignore, allowing you to list words not to be
2126
   * capitalized.
2127
   *
2128
   * Adapted from John Gruber's script.
2129
   *
2130
   * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
2131
   *
2132
   * @param array $ignore <p>An array of words not to capitalize.</p>
2133
   *
2134
   * @return static <p>Object with a titleized $str</p>
2135
   */
2136 35
  public function titleizeForHumans(array $ignore = []): self
2137
  {
2138 35
    $smallWords = \array_merge(
2139
        [
2140 35
            '(?<!q&)a',
2141
            'an',
2142
            'and',
2143
            'as',
2144
            'at(?!&t)',
2145
            'but',
2146
            'by',
2147
            'en',
2148
            'for',
2149
            'if',
2150
            'in',
2151
            'of',
2152
            'on',
2153
            'or',
2154
            'the',
2155
            'to',
2156
            'v[.]?',
2157
            'via',
2158
            'vs[.]?',
2159
        ],
2160 35
        $ignore
2161
    );
2162
2163 35
    $smallWordsRx = \implode('|', $smallWords);
2164 35
    $apostropheRx = '(?x: [\'’] [[:lower:]]* )?';
2165 35
    $stringy = static::create($this->trim(), $this->encoding);
2166
2167 35
    if (\preg_match('/[[:lower:]]/', (string)$stringy) === 0) {
2168 3
      $stringy = $stringy->toLowerCase();
2169
    }
2170
2171
    // The main substitutions
2172 35
    $stringy->str = \preg_replace_callback(
2173
        '~\b (_*) (?:                                                              # 1. Leading underscore and
2174
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |              # 2. file path or 
2175 35
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostropheRx . ' ) #    URL, domain, or email
2176
                        |
2177 35
                        ( (?i: ' . $smallWordsRx . ' ) ' . $apostropheRx . ' )            # 3. or small word (case-insensitive)
2178
                        |
2179 35
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostropheRx . ' )     # 4. or word w/o internal caps
2180
                        |
2181 35
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostropheRx . ' )     # 5. or some other word
2182
                      ) (_*) \b                                                           # 6. With trailing underscore
2183
                    ~ux',
2184 35
        function ($matches) {
2185
          // Preserve leading underscore
2186 35
          $str = $matches[1];
2187 35
          if ($matches[2]) {
2188
            // Preserve URLs, domains, emails and file paths
2189 5
            $str .= $matches[2];
2190 35
          } elseif ($matches[3]) {
2191
            // Lower-case small words
2192 25
            $str .= static::create($matches[3], $this->encoding)->toLowerCase();
2193 35
          } elseif ($matches[4]) {
2194
            // Capitalize word w/o internal caps
2195 34
            $str .= static::create($matches[4], $this->encoding)->upperCaseFirst();
2196
          } else {
2197
            // Preserve other kinds of word (iPhone)
2198 7
            $str .= $matches[5];
2199
          }
2200
          // Preserve trailing underscore
2201 35
          $str .= $matches[6];
2202
2203 35
          return $str;
2204 35
        },
2205 35
        $stringy->str
2206
    );
2207
2208
    // Exceptions for small words: capitalize at start of title...
2209 35
    $stringy->str = \preg_replace_callback(
2210
        '~(  \A [[:punct:]]*                # start of title...
2211
                      |  [:.;?!][ ]+               # or of subsentence...
2212
                      |  [ ][\'"“‘(\[][ ]* )       # or of inserted subphrase...
2213 35
                      ( ' . $smallWordsRx . ' ) \b # ...followed by small word
2214
                     ~uxi',
2215 35
        function ($matches) {
2216 11
          return $matches[1] . static::create($matches[2], $this->encoding)->upperCaseFirst();
2217 35
        },
2218 35
        $stringy->str
2219
    );
2220
2221
    // ...and end of title
2222 35
    $stringy->str = \preg_replace_callback(
2223 35
        '~\b ( ' . $smallWordsRx . ' ) # small word...
2224
                      (?= [[:punct:]]* \Z     # ...at the end of the title...
2225
                      |   [\'"’”)\]] [ ] )    # ...or of an inserted subphrase?
2226
                     ~uxi',
2227 35
        function ($matches) {
2228 3
          return static::create($matches[1], $this->encoding)->upperCaseFirst();
2229 35
        },
2230 35
        $stringy->str
2231
    );
2232
2233
    // Exceptions for small words in hyphenated compound words
2234
    // e.g. "in-flight" -> In-Flight
2235 35
    $stringy->str = \preg_replace_callback(
2236
        '~\b
2237
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
2238 35
                        ( ' . $smallWordsRx . ' )
2239
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
2240
                       ~uxi',
2241 35
        function ($matches) {
2242
          return static::create($matches[1], $this->encoding)->upperCaseFirst();
2243 35
        },
2244 35
        $stringy->str
2245
    );
2246
2247
    // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
2248 35
    $stringy->str = \preg_replace_callback(
2249
        '~\b
2250
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
2251
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
2252 35
                      ( ' . $smallWordsRx . ' ) # ...followed by small word
2253
                      (?!	- )                   # Negative lookahead for another -
2254
                     ~uxi',
2255 35
        function ($matches) {
2256
          return $matches[1] . static::create($matches[2], $this->encoding)->upperCaseFirst();
2257 35
        },
2258 35
        $stringy->str
2259
    );
2260
2261 35
    return $stringy;
2262
  }
2263
2264
  /**
2265
   * Returns a trimmed string with the first letter of each word capitalized.
2266
   * Also accepts an array, $ignore, allowing you to list words not to be
2267
   * capitalized.
2268
   *
2269
   * @param array|null $ignore [optional] <p>An array of words not to capitalize or null. Default: null</p>
2270
   *
2271
   * @return static <p>Object with a titleized $str.</p>
2272
   */
2273 5
  public function titleize(array $ignore = null): self
2274
  {
2275 5
    $stringy = static::create($this->trim(), $this->encoding);
2276 5
    $encoding = $this->encoding;
2277
2278 5
    $stringy->str = (string)\preg_replace_callback(
2279 5
        '/([\S]+)/u',
2280 5
        function ($match) use ($encoding, $ignore) {
2281 5
          if ($ignore && \in_array($match[0], $ignore, true)) {
2282 2
            return $match[0];
2283
          }
2284
2285 5
          $stringy = new static($match[0], $encoding);
2286
2287 5
          return (string)$stringy->toLowerCase()->upperCaseFirst();
2288 5
        },
2289 5
        $stringy->str
2290
    );
2291
2292 5
    return $stringy;
2293
  }
2294
2295
  /**
2296
   * Converts all characters in the string to lowercase.
2297
   *
2298
   * @return static <p>Object with all characters of $str being lowercase.</p>
2299
   */
2300 54
  public function toLowerCase(): self
2301
  {
2302 54
    $str = UTF8::strtolower($this->str, $this->encoding);
2303
2304 54
    return static::create($str, $this->encoding);
2305
  }
2306
2307
  /**
2308
   * Returns true if the string is base64 encoded, false otherwise.
2309
   *
2310
   * @return bool <p>Whether or not $str is base64 encoded.</p>
2311
   */
2312 7
  public function isBase64(): bool
2313
  {
2314 7
    return UTF8::is_base64($this->str);
2315
  }
2316
2317
  /**
2318
   * Returns an ASCII version of the string. A set of non-ASCII characters are
2319
   * replaced with their closest ASCII counterparts, and the rest are removed
2320
   * unless instructed otherwise.
2321
   *
2322
   * @param bool $strict [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad performance |
2323
   *                     Default: false</p>
2324
   *
2325
   * @return static <p>Object whose $str contains only ASCII characters.</p>
2326
   */
2327 16
  public function toAscii(bool $strict = false): self
2328
  {
2329 16
    $str = UTF8::to_ascii($this->str, '?', $strict);
2330
2331 16
    return static::create($str, $this->encoding);
2332
  }
2333
2334
  /**
2335
   * Returns a boolean representation of the given logical string value.
2336
   * For example, 'true', '1', 'on' and 'yes' will return true. 'false', '0',
2337
   * 'off', and 'no' will return false. In all instances, case is ignored.
2338
   * For other numeric strings, their sign will determine the return value.
2339
   * In addition, blank strings consisting of only whitespace will return
2340
   * false. For all other strings, the return value is a result of a
2341
   * boolean cast.
2342
   *
2343
   * @return bool <p>A boolean value for the string.</p>
2344
   */
2345 15
  public function toBoolean(): bool
2346
  {
2347 15
    $key = $this->toLowerCase()->str;
2348
    $map = [
2349 15
        'true'  => true,
2350
        '1'     => true,
2351
        'on'    => true,
2352
        'yes'   => true,
2353
        'false' => false,
2354
        '0'     => false,
2355
        'off'   => false,
2356
        'no'    => false,
2357
    ];
2358
2359 15
    if (\array_key_exists($key, $map)) {
2360 10
      return $map[$key];
2361
    }
2362
2363 5
    if (\is_numeric($this->str)) {
2364 2
      return ((int)$this->str > 0);
2365
    }
2366
2367 3
    return (bool)$this->regexReplace('[[:space:]]', '')->str;
2368
  }
2369
2370
  /**
2371
   * Return Stringy object as string, but you can also use (string) for automatically casting the object into a string.
2372
   *
2373
   * @return string
2374
   */
2375 1076
  public function toString(): string
2376
  {
2377 1076
    return (string)$this->str;
2378
  }
2379
2380
  /**
2381
   * Converts each tab in the string to some number of spaces, as defined by
2382
   * $tabLength. By default, each tab is converted to 4 consecutive spaces.
2383
   *
2384
   * @param int $tabLength [optional] <p>Number of spaces to replace each tab with. Default: 4</p>
2385
   *
2386
   * @return static <p>Object whose $str has had tabs switched to spaces.</p>
2387
   */
2388 6 View Code Duplication
  public function toSpaces(int $tabLength = 4): self
0 ignored issues
show
Duplication introduced by
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...
2389
  {
2390 6
    $spaces = UTF8::str_repeat(' ', $tabLength);
2391 6
    $str = UTF8::str_replace("\t", $spaces, $this->str);
2392
2393 6
    return static::create($str, $this->encoding);
2394
  }
2395
2396
  /**
2397
   * Converts each occurrence of some consecutive number of spaces, as
2398
   * defined by $tabLength, to a tab. By default, each 4 consecutive spaces
2399
   * are converted to a tab.
2400
   *
2401
   * @param int $tabLength [optional] <p>Number of spaces to replace with a tab. Default: 4</p>
2402
   *
2403
   * @return static <p>Object whose $str has had spaces switched to tabs.</p>
2404
   */
2405 5 View Code Duplication
  public function toTabs(int $tabLength = 4): self
0 ignored issues
show
Duplication introduced by
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...
2406
  {
2407 5
    $spaces = UTF8::str_repeat(' ', $tabLength);
2408 5
    $str = UTF8::str_replace($spaces, "\t", $this->str);
2409
2410 5
    return static::create($str, $this->encoding);
2411
  }
2412
2413
  /**
2414
   * Converts the first character of each word in the string to uppercase.
2415
   *
2416
   * @return static  Object with all characters of $str being title-cased
2417
   */
2418 5
  public function toTitleCase(): self
2419
  {
2420
    // "mb_convert_case()" used a polyfill from the "UTF8"-Class
2421 5
    $str = \mb_convert_case($this->str, MB_CASE_TITLE, $this->encoding);
2422
2423 5
    return static::create($str, $this->encoding);
2424
  }
2425
2426
  /**
2427
   * Converts all characters in the string to uppercase.
2428
   *
2429
   * @return static  Object with all characters of $str being uppercase
2430
   */
2431 5
  public function toUpperCase(): self
2432
  {
2433 5
    $str = UTF8::strtoupper($this->str, $this->encoding);
2434
2435 5
    return static::create($str, $this->encoding);
2436
  }
2437
2438
  /**
2439
   * Returns a string with whitespace removed from the start of the string.
2440
   * Supports the removal of unicode whitespace. Accepts an optional
2441
   * string of characters to strip instead of the defaults.
2442
   *
2443
   * @param string $chars [optional] <p>Optional string of characters to strip. Default: null</p>
2444
   *
2445
   * @return static <p>Object with a trimmed $str.</p>
2446
   */
2447 13 View Code Duplication
  public function trimLeft(string $chars = null): self
0 ignored issues
show
Duplication introduced by
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...
2448
  {
2449 13
    if (!$chars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chars of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2450 11
      $chars = '[:space:]';
2451
    } else {
2452 2
      $chars = \preg_quote($chars, '/');
2453
    }
2454
2455 13
    return $this->regexReplace("^[$chars]+", '');
2456
  }
2457
2458
  /**
2459
   * Returns a string with whitespace removed from the end of the string.
2460
   * Supports the removal of unicode whitespace. Accepts an optional
2461
   * string of characters to strip instead of the defaults.
2462
   *
2463
   * @param string $chars [optional] <p>Optional string of characters to strip. Default: null</p>
2464
   *
2465
   * @return static <p>Object with a trimmed $str.</p>
2466
   */
2467 13 View Code Duplication
  public function trimRight(string $chars = null): self
0 ignored issues
show
Duplication introduced by
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...
2468
  {
2469 13
    if (!$chars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chars of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2470 11
      $chars = '[:space:]';
2471
    } else {
2472 2
      $chars = \preg_quote($chars, '/');
2473
    }
2474
2475 13
    return $this->regexReplace("[$chars]+\$", '');
2476
  }
2477
2478
  /**
2479
   * Truncates the string to a given length. If $substring is provided, and
2480
   * truncating occurs, the string is further truncated so that the substring
2481
   * may be appended without exceeding the desired length.
2482
   *
2483
   * @param int    $length    <p>Desired length of the truncated string.</p>
2484
   * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
2485
   *
2486
   * @return static <p>Object with the resulting $str after truncating.</p>
2487
   */
2488 22 View Code Duplication
  public function truncate(int $length, string $substring = ''): self
0 ignored issues
show
Duplication introduced by
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...
2489
  {
2490 22
    $stringy = static::create($this->str, $this->encoding);
2491 22
    if ($length >= $stringy->length()) {
2492 4
      return $stringy;
2493
    }
2494
2495
    // Need to further trim the string so we can append the substring
2496 18
    $substringLength = UTF8::strlen($substring, $stringy->encoding);
2497 18
    $length -= $substringLength;
2498
2499 18
    $truncated = UTF8::substr($stringy->str, 0, $length, $stringy->encoding);
2500 18
    $stringy->str = $truncated . $substring;
2501
2502 18
    return $stringy;
2503
  }
2504
2505
  /**
2506
   * Returns a lowercase and trimmed string separated by underscores.
2507
   * Underscores are inserted before uppercase characters (with the exception
2508
   * of the first character of the string), and in place of spaces as well as
2509
   * dashes.
2510
   *
2511
   * @return static <p>Object with an underscored $str.</p>
2512
   */
2513 16
  public function underscored(): self
2514
  {
2515 16
    return $this->delimit('_');
2516
  }
2517
2518
  /**
2519
   * Returns an UpperCamelCase version of the supplied string. It trims
2520
   * surrounding spaces, capitalizes letters following digits, spaces, dashes
2521
   * and underscores, and removes spaces, dashes, underscores.
2522
   *
2523
   * @return static  <p>Object with $str in UpperCamelCase.</p>
2524
   */
2525 13
  public function upperCamelize(): self
2526
  {
2527 13
    return $this->camelize()->upperCaseFirst();
2528
  }
2529
2530
  /**
2531
   * Returns a camelCase version of the string. Trims surrounding spaces,
2532
   * capitalizes letters following digits, spaces, dashes and underscores,
2533
   * and removes spaces, dashes, as well as underscores.
2534
   *
2535
   * @return static <p>Object with $str in camelCase.</p>
2536
   */
2537 32
  public function camelize(): self
2538
  {
2539 32
    $encoding = $this->encoding;
2540 32
    $stringy = $this->trim()->lowerCaseFirst();
2541 32
    $stringy->str = (string)\preg_replace('/^[-_]+/', '', $stringy->str);
2542
2543 32
    $stringy->str = (string)\preg_replace_callback(
2544 32
        '/[-_\s]+(.)?/u',
2545 32
        function ($match) use ($encoding) {
2546 27
          if (isset($match[1])) {
2547 27
            return UTF8::strtoupper($match[1], $encoding);
2548
          }
2549
2550 1
          return '';
2551 32
        },
2552 32
        $stringy->str
2553
    );
2554
2555 32
    $stringy->str = (string)\preg_replace_callback(
2556 32
        '/[\d]+(.)?/u',
2557 32
        function ($match) use ($encoding) {
2558 6
          return UTF8::strtoupper($match[0], $encoding);
2559 32
        },
2560 32
        $stringy->str
2561
    );
2562
2563 32
    return $stringy;
2564
  }
2565
2566
  /**
2567
   * Convert a string to e.g.: "snake_case"
2568
   *
2569
   * @return static <p>Object with $str in snake_case.</p>
2570
   */
2571 20
  public function snakeize(): self
2572
  {
2573 20
    $str = $this->str;
2574
2575 20
    $encoding = $this->encoding;
2576 20
    $str = UTF8::normalize_whitespace($str);
2577 20
    $str = \str_replace('-', '_', $str);
2578
2579 20
    $str = (string)\preg_replace_callback(
2580 20
        '/([\d|A-Z])/u',
2581 20
        function ($matches) use ($encoding) {
2582 8
          $match = $matches[1];
2583 8
          $matchInt = (int)$match;
2584
2585 8
          if ("$matchInt" == $match) {
2586 4
            return '_' . $match . '_';
2587
          }
2588
2589 4
          return '_' . UTF8::strtolower($match, $encoding);
2590 20
        },
2591 20
        $str
2592
    );
2593
2594 20
    $str = (string)\preg_replace(
2595
        [
2596
2597 20
            '/\s+/',      // convert spaces to "_"
2598
            '/^\s+|\s+$/',  // trim leading & trailing spaces
2599
            '/_+/',         // remove double "_"
2600
        ],
2601
        [
2602 20
            '_',
2603
            '',
2604
            '_',
2605
        ],
2606 20
        $str
2607
    );
2608
2609 20
    $str = UTF8::trim($str, '_'); // trim leading & trailing "_"
2610 20
    $str = UTF8::trim($str); // trim leading & trailing whitespace
2611
2612 20
    return static::create($str, $this->encoding);
2613
  }
2614
2615
  /**
2616
   * Converts the first character of the string to lower case.
2617
   *
2618
   * @return static <p>Object with the first character of $str being lower case.</p>
2619
   */
2620 37 View Code Duplication
  public function lowerCaseFirst(): self
0 ignored issues
show
Duplication introduced by
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...
2621
  {
2622 37
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
2623 37
    $rest = UTF8::substr($this->str, 1, $this->length() - 1, $this->encoding);
2624
2625 37
    $str = UTF8::strtolower($first, $this->encoding) . $rest;
0 ignored issues
show
Security Bug introduced by
It seems like $first defined by \voku\helper\UTF8::subst... 0, 1, $this->encoding) on line 2622 can also be of type false; however, voku\helper\UTF8::strtolower() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2626
2627 37
    return static::create($str, $this->encoding);
2628
  }
2629
2630
  /**
2631
   * Shorten the string after $length, but also after the next word.
2632
   *
2633
   * @param int    $length
2634
   * @param string $strAddOn [optional] <p>Default: '…'</p>
2635
   *
2636
   * @return static
2637
   */
2638 4
  public function shortenAfterWord(int $length, string $strAddOn = '…'): self
2639
  {
2640 4
    $string = UTF8::str_limit_after_word($this->str, $length, $strAddOn);
2641
2642 4
    return static::create($string);
2643
  }
2644
2645
  /**
2646
   * Line-Wrap the string after $limit, but also after the next word.
2647
   *
2648
   * @param int $limit
2649
   *
2650
   * @return static
2651
   */
2652 1
  public function lineWrapAfterWord(int $limit): self
2653
  {
2654 1
    $strings = (array)\preg_split('/\\r\\n|\\r|\\n/', $this->str);
2655
2656 1
    $string = '';
2657 1
    foreach ($strings as $value) {
2658 1
      $string .= wordwrap($value, $limit);
2659 1
      $string .= "\n";
2660
    }
2661
2662 1
    return static::create($string);
2663
  }
2664
2665
  /**
2666
   * Gets the substring after the first occurrence of a separator.
2667
   * If no match is found returns new empty Stringy object.
2668
   *
2669
   * @param string $separator
2670
   *
2671
   * @return static
2672
   */
2673 2 View Code Duplication
  public function afterFirst(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2674
  {
2675 2
    if ($separator === '') {
2676
      return static::create();
2677
    }
2678
2679 2
    if ($this->str === '') {
2680 1
      return static::create();
2681
    }
2682
2683 2
    if (($offset = $this->indexOf($separator)) === false) {
2684 2
      return static::create();
2685
    }
2686
2687 2
    return static::create(
2688 2
        UTF8::substr(
2689 2
            $this->str,
2690 2
            $offset + UTF8::strlen($separator, $this->encoding),
2691 2
            null,
2692 2
            $this->encoding
2693
        ),
2694 2
        $this->encoding
2695
    );
2696
  }
2697
2698
  /**
2699
   * Gets the substring after the first occurrence of a separator.
2700
   * If no match is found returns new empty Stringy object.
2701
   *
2702
   * @param string $separator
2703
   *
2704
   * @return static
2705
   */
2706 1 View Code Duplication
  public function afterFirstIgnoreCase(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2707
  {
2708 1
    if ($separator === '') {
2709
      return static::create();
2710
    }
2711
2712 1
    if ($this->str === '') {
2713 1
      return static::create();
2714
    }
2715
2716 1
    if (($offset = $this->indexOfIgnoreCase($separator)) === false) {
2717 1
      return static::create();
2718
    }
2719
2720 1
    return static::create(
2721 1
        UTF8::substr(
2722 1
            $this->str,
2723 1
            $offset + UTF8::strlen($separator, $this->encoding),
2724 1
            null,
2725 1
            $this->encoding
2726
        ),
2727 1
        $this->encoding
2728
    );
2729
  }
2730
2731
  /**
2732
   * Gets the substring after the last occurrence of a separator.
2733
   * If no match is found returns new empty Stringy object.
2734
   *
2735
   * @param string $separator
2736
   *
2737
   * @return static
2738
   */
2739 1 View Code Duplication
  public function afterLastIgnoreCase(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2740
  {
2741 1
    if ($separator === '') {
2742
      return static::create();
2743
    }
2744
2745 1
    if ($this->str === '') {
2746 1
      return static::create();
2747
    }
2748
2749 1
    $offset = $this->indexOfLastIgnoreCase($separator);
2750 1
    if ($offset === false) {
2751 1
      return static::create('', $this->encoding);
2752
    }
2753
2754 1
    return static::create(
2755 1
        UTF8::substr(
2756 1
            $this->str,
2757 1
            $offset + UTF8::strlen($separator, $this->encoding),
2758 1
            null,
2759 1
            $this->encoding
2760
        ),
2761 1
        $this->encoding
2762
    );
2763
  }
2764
2765
  /**
2766
   * Gets the substring after the last occurrence of a separator.
2767
   * If no match is found returns new empty Stringy object.
2768
   *
2769
   * @param string $separator
2770
   *
2771
   * @return static
2772
   */
2773 1 View Code Duplication
  public function afterLast(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2774
  {
2775 1
    if ($separator === '') {
2776
      return static::create();
2777
    }
2778
2779 1
    if ($this->str === '') {
2780 1
      return static::create();
2781
    }
2782
2783 1
    $offset = $this->indexOfLast($separator);
2784 1
    if ($offset === false) {
2785 1
      return static::create('', $this->encoding);
2786
    }
2787
2788 1
    return static::create(
2789 1
        UTF8::substr(
2790 1
            $this->str,
2791 1
            $offset + UTF8::strlen($separator, $this->encoding),
2792 1
            null,
2793 1
            $this->encoding
2794
        ),
2795 1
        $this->encoding
2796
    );
2797
  }
2798
2799
  /**
2800
   * Gets the substring before the first occurrence of a separator.
2801
   * If no match is found returns new empty Stringy object.
2802
   *
2803
   * @param string $separator
2804
   *
2805
   * @return static
2806
   */
2807 1 View Code Duplication
  public function beforeFirst(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2808
  {
2809 1
    if ($separator === '') {
2810
      return static::create();
2811
    }
2812
2813 1
    if ($this->str === '') {
2814 1
      return static::create();
2815
    }
2816
2817 1
    $offset = $this->indexOf($separator);
2818 1
    if ($offset === false) {
2819 1
      return static::create('', $this->encoding);
2820
    }
2821
2822 1
    return static::create(
2823 1
        UTF8::substr(
2824 1
            $this->str,
2825 1
            0,
2826 1
            $offset,
2827 1
            $this->encoding
2828
        ),
2829 1
        $this->encoding
2830
    );
2831
  }
2832
2833
  /**
2834
   * Gets the substring before the first occurrence of a separator.
2835
   * If no match is found returns new empty Stringy object.
2836
   *
2837
   * @param string $separator
2838
   *
2839
   * @return static
2840
   */
2841 1 View Code Duplication
  public function beforeFirstIgnoreCase(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2842
  {
2843 1
    if ($separator === '') {
2844
      return static::create();
2845
    }
2846
2847 1
    if ($this->str === '') {
2848 1
      return static::create();
2849
    }
2850
2851 1
    $offset = $this->indexOfIgnoreCase($separator);
2852 1
    if ($offset === false) {
2853 1
      return static::create('', $this->encoding);
2854
    }
2855
2856 1
    return static::create(
2857 1
        UTF8::substr(
2858 1
            $this->str,
2859 1
            0,
2860 1
            $offset,
2861 1
            $this->encoding
2862
        ),
2863 1
        $this->encoding
2864
    );
2865
  }
2866
2867
  /**
2868
   * Gets the substring before the last occurrence of a separator.
2869
   * If no match is found returns new empty Stringy object.
2870
   *
2871
   * @param string $separator
2872
   *
2873
   * @return static
2874
   */
2875 1 View Code Duplication
  public function beforeLast(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2876
  {
2877 1
    if ($separator === '') {
2878
      return static::create();
2879
    }
2880
2881 1
    if ($this->str === '') {
2882 1
      return static::create();
2883
    }
2884
2885 1
    $offset = $this->indexOfLast($separator);
2886 1
    if ($offset === false) {
2887 1
      return static::create('', $this->encoding);
2888
    }
2889
2890 1
    return static::create(
2891 1
        UTF8::substr(
2892 1
            $this->str,
2893 1
            0,
2894 1
            $offset,
2895 1
            $this->encoding
2896
        ),
2897 1
        $this->encoding
2898
    );
2899
  }
2900
2901
  /**
2902
   * Gets the substring before the last occurrence of a separator.
2903
   * If no match is found returns new empty Stringy object.
2904
   *
2905
   * @param string $separator
2906
   *
2907
   * @return static
2908
   */
2909 1 View Code Duplication
  public function beforeLastIgnoreCase(string $separator): self
0 ignored issues
show
Duplication introduced by
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...
2910
  {
2911 1
    if ($separator === '') {
2912
      return static::create();
2913
    }
2914
2915 1
    if ($this->str === '') {
2916 1
      return static::create();
2917
    }
2918
2919 1
    $offset = $this->indexOfLastIgnoreCase($separator);
2920 1
    if ($offset === false) {
2921 1
      return static::create('', $this->encoding);
2922
    }
2923
2924 1
    return static::create(
2925 1
        UTF8::substr(
2926 1
            $this->str,
2927 1
            0,
2928 1
            $offset,
2929 1
            $this->encoding
2930
        ),
2931 1
        $this->encoding
2932
    );
2933
  }
2934
2935
  /**
2936
   * Returns the string with the first letter of each word capitalized,
2937
   * except for when the word is a name which shouldn't be capitalized.
2938
   *
2939
   * @return static <p>Object with $str capitalized.</p>
2940
   */
2941 39
  public function capitalizePersonalName(): self
2942
  {
2943 39
    $stringy = $this->collapseWhitespace();
2944 39
    $stringy->str = $this->capitalizePersonalNameByDelimiter($stringy->str, ' ')->toString();
2945 39
    $stringy->str = $this->capitalizePersonalNameByDelimiter($stringy->str, '-')->toString();
2946
2947 39
    return static::create($stringy, $this->encoding);
2948
  }
2949
2950
  /**
2951
   * @param string $word
2952
   *
2953
   * @return static <p>Object with $str capitalized.</p>
2954
   */
2955 7
  protected function capitalizeWord(string $word): self
2956
  {
2957 7
    $encoding = $this->encoding;
2958
2959 7
    $firstCharacter = UTF8::substr($word, 0, 1, $encoding);
2960 7
    $restOfWord = UTF8::substr($word, 1, null, $encoding);
2961 7
    $firstCharacterUppercased = UTF8::strtoupper($firstCharacter, $encoding);
0 ignored issues
show
Security Bug introduced by
It seems like $firstCharacter defined by \voku\helper\UTF8::substr($word, 0, 1, $encoding) on line 2959 can also be of type false; however, voku\helper\UTF8::strtoupper() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2962
2963 7
    return static::create($firstCharacterUppercased . $restOfWord, $encoding);
2964
  }
2965
2966
  /**
2967
   * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
2968
   *
2969
   * @param string $names
2970
   * @param string $delimiter
2971
   *
2972
   * @return static
2973
   */
2974 39
  protected function capitalizePersonalNameByDelimiter(string $names, string $delimiter): self
2975
  {
2976
    // init
2977 39
    $namesArray = \explode($delimiter, $names);
2978 39
    $encoding = $this->encoding;
2979
2980
    $specialCases = [
2981 39
        'names'    => [
2982
            'ab',
2983
            'af',
2984
            'al',
2985
            'and',
2986
            'ap',
2987
            'bint',
2988
            'binte',
2989
            'da',
2990
            'de',
2991
            'del',
2992
            'den',
2993
            'der',
2994
            'di',
2995
            'dit',
2996
            'ibn',
2997
            'la',
2998
            'mac',
2999
            'nic',
3000
            'of',
3001
            'ter',
3002
            'the',
3003
            'und',
3004
            'van',
3005
            'von',
3006
            'y',
3007
            'zu',
3008
        ],
3009
        'prefixes' => [
3010
            'al-',
3011
            "d'",
3012
            'ff',
3013
            "l'",
3014
            'mac',
3015
            'mc',
3016
            'nic',
3017
        ],
3018
    ];
3019
3020 39
    foreach ($namesArray as &$name) {
3021 39
      if (\in_array($name, $specialCases['names'], true)) {
3022 27
        continue;
3023
      }
3024
3025 13
      $continue = false;
3026
3027 13
      if ($delimiter == '-') {
3028 13 View Code Duplication
        foreach ($specialCases['names'] as $beginning) {
0 ignored issues
show
Duplication introduced by
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...
3029 13
          if (UTF8::strpos($name, $beginning, 0, $encoding) === 0) {
3030 13
            $continue = true;
3031
          }
3032
        }
3033
      }
3034
3035 13 View Code Duplication
      foreach ($specialCases['prefixes'] as $beginning) {
0 ignored issues
show
Duplication introduced by
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...
3036 13
        if (UTF8::strpos($name, $beginning, 0, $encoding) === 0) {
3037 13
          $continue = true;
3038
        }
3039
      }
3040
3041 13
      if ($continue) {
3042 7
        continue;
3043
      }
3044
3045 7
      $name = $this->capitalizeWord($name);
3046
    }
3047
3048 39
    return static::create(\implode($delimiter, $namesArray), $encoding);
3049
  }
3050
}
3051