Completed
Push — master ( 8b0b33...cbeae3 )
by Lars
06:04 queued 04:26
created

Stringy::trimRight()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 10
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 5
cts 5
cp 1
crap 2
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.</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
    // init
66 1150
    UTF8::checkForSupport();
67
68 1150
    $this->str = (string)$str;
69
70 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...
71 899
      $this->encoding = $encoding;
72
    } else {
73 775
      $this->encoding = \mb_internal_encoding();
74
    }
75 1150
  }
76
77
  /**
78
   * Returns the value in $str.
79
   *
80
   * @return string <p>The current value of the $str property.</p>
81
   */
82 221
  public function __toString()
83
  {
84 221
    return (string)$this->str;
85
  }
86
87
  /**
88
   * Returns a new string with $string appended.
89
   *
90
   * @param string $string <p>The string to append.</p>
91
   *
92
   * @return static <p>Object with appended $string.</p>
93
   */
94 5
  public function append(string $string): self
95
  {
96 5
    return static::create($this->str . $string, $this->encoding);
97
  }
98
99
  /**
100
   * Append an password (limited to chars that are good readable).
101
   *
102
   * @param int $length <p>Length of the random string.</p>
103
   *
104
   * @return static <p>Object with appended password.</p>
105
   */
106 1
  public function appendPassword(int $length): self
107
  {
108 1
    $possibleChars = '2346789bcdfghjkmnpqrtvwxyzBCDFGHJKLMNPQRTVWXYZ';
109
110 1
    return $this->appendRandomString($length, $possibleChars);
111
  }
112
113
  /**
114
   * Append an unique identifier.
115
   *
116
   * @param string|int $entropyExtra [optional] <p>Extra entropy via a string or int value.</p>
117
   * @param bool       $md5          [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
118
   *
119
   * @return static <p>Object with appended unique identifier as md5-hash.</p>
120
   */
121 1
  public function appendUniqueIdentifier($entropyExtra = '', bool $md5 = true): self
122
  {
123 1
    $uniqueHelper = \mt_rand() .
124 1
                    \session_id() .
125 1
                    ($_SERVER['REMOTE_ADDR'] ?? '') .
126 1
                    ($_SERVER['SERVER_ADDR'] ?? '') .
127 1
                    $entropyExtra;
128
129 1
    $uniqueString = \uniqid($uniqueHelper, true);
130
131 1
    if ($md5) {
132 1
      $uniqueString = \md5($uniqueString . $uniqueHelper);
133
    }
134
135 1
    return $this->append($uniqueString);
136
  }
137
138
  /**
139
   * Append an random string.
140
   *
141
   * @param int    $length        <p>Length of the random string.</p>
142
   * @param string $possibleChars [optional] <p>Characters string for the random selection.</p>
143
   *
144
   * @return static <p>Object with appended random string.</p>
145
   */
146 2
  public function appendRandomString(int $length, string $possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'): self
147
  {
148
    // init
149 2
    $i = 0;
150 2
    $str = $this->str;
151 2
    $maxlength = UTF8::strlen($possibleChars, $this->encoding);
152
153 2
    if ($maxlength === 0) {
154 1
      return $this;
155
    }
156
157
    // add random chars
158 2
    while ($i < $length) {
159 2
      $char = UTF8::substr($possibleChars, random_int(0, $maxlength - 1), 1, $this->encoding);
160 2
      $str .= $char;
161 2
      $i++;
162
    }
163
164 2
    return $this->append($str);
165
  }
166
167
  /**
168
   * Creates a Stringy object and assigns both str and encoding properties
169
   * the supplied values. $str is cast to a string prior to assignment, and if
170
   * $encoding is not specified, it defaults to mb_internal_encoding(). It
171
   * then returns the initialized object. Throws an InvalidArgumentException
172
   * if the first argument is an array or object without a __toString method.
173
   *
174
   * @param  mixed  $str      [optional] <p>Value to modify, after being cast to string. Default: ''</p>
175
   * @param  string $encoding [optional] <p>The character encoding.</p>
176
   *
177
   * @return static <p>A Stringy object.</p>
178
   *
179
   * @throws \InvalidArgumentException <p>if an array or object without a
180
   *         __toString method is passed as the first argument</p>
181
   */
182 1142
  public static function create($str = '', string $encoding = null): self
183
  {
184 1142
    return new static($str, $encoding);
185
  }
186
187
  /**
188
   * Returns the substring between $start and $end, if found, or an empty
189
   * string. An optional offset may be supplied from which to begin the
190
   * search for the start string.
191
   *
192
   * @param  string $start  <p>Delimiter marking the start of the substring.</p>
193
   * @param  string $end    <p>Delimiter marking the end of the substring.</p>
194
   * @param  int    $offset [optional] <p>Index from which to begin the search. Default: 0</p>
195
   *
196
   * @return static <p>Object whose $str is a substring between $start and $end.</p>
197
   */
198 16
  public function between(string $start, string $end, int $offset = 0): self
199
  {
200 16
    $startIndex = $this->indexOf($start, $offset);
201 16
    if ($startIndex === false) {
202 2
      return static::create('', $this->encoding);
203
    }
204
205 14
    $substrIndex = $startIndex + UTF8::strlen($start, $this->encoding);
206 14
    $endIndex = $this->indexOf($end, $substrIndex);
207 14
    if ($endIndex === false) {
208 2
      return static::create('', $this->encoding);
209
    }
210
211 12
    return $this->substr($substrIndex, $endIndex - $substrIndex);
212
  }
213
214
  /**
215
   * Returns the index of the first occurrence of $needle in the string,
216
   * and false if not found. Accepts an optional offset from which to begin
217
   * the search.
218
   *
219
   * @param  string $needle <p>Substring to look for.</p>
220
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
221
   *
222
   * @return int|false <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
223
   */
224 29
  public function indexOf(string $needle, int $offset = 0)
225
  {
226 29
    return UTF8::strpos($this->str, $needle, $offset, $this->encoding);
227
  }
228
229
  /**
230
   * Returns the index of the first occurrence of $needle in the string,
231
   * and false if not found. Accepts an optional offset from which to begin
232
   * the search.
233
   *
234
   * @param  string $needle <p>Substring to look for.</p>
235
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
236
   *
237
   * @return int|false <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
238
   */
239 2
  public function indexOfIgnoreCase(string $needle, int $offset = 0)
240
  {
241 2
    return UTF8::stripos($this->str, $needle, $offset, $this->encoding);
242
  }
243
244
  /**
245
   * Returns the substring beginning at $start with the specified $length.
246
   * It differs from the UTF8::substr() function in that providing a $length of
247
   * null will return the rest of the string, rather than an empty string.
248
   *
249
   * @param int $start  <p>Position of the first character to use.</p>
250
   * @param int $length [optional] <p>Maximum number of characters used. Default: null</p>
251
   *
252
   * @return static <p>Object with its $str being the substring.</p>
253
   */
254 65
  public function substr(int $start, int $length = null): self
255
  {
256 65
    if ($length === null) {
257 19
      $length = $this->length();
258
    }
259
260 65
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
261
262 65
    return static::create($str, $this->encoding);
263
  }
264
265
  /**
266
   * Returns the length of the string.
267
   *
268
   * @return int <p>The number of characters in $str given the encoding.</p>
269
   */
270 293
  public function length(): int
271
  {
272 293
    return UTF8::strlen($this->str, $this->encoding);
273
  }
274
275
  /**
276
   * Trims the string and replaces consecutive whitespace characters with a
277
   * single space. This includes tabs and newline characters, as well as
278
   * multibyte whitespace such as the thin space and ideographic space.
279
   *
280
   * @return static <p>Object with a trimmed $str and condensed whitespace.</p>
281
   */
282 52
  public function collapseWhitespace(): self
283
  {
284 52
    return $this->regexReplace('[[:space:]]+', ' ')->trim();
285
  }
286
287
  /**
288
   * Returns a string with whitespace removed from the start and end of the
289
   * string. Supports the removal of unicode whitespace. Accepts an optional
290
   * string of characters to strip instead of the defaults.
291
   *
292
   * @param string $chars [optional] <p>String of characters to strip. Default: null</p>
293
   *
294
   * @return static <p>Object with a trimmed $str.</p>
295
   */
296 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...
297
  {
298 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...
299 187
      $chars = '[:space:]';
300
    } else {
301 1
      $chars = \preg_quote($chars, '/');
302
    }
303
304 188
    return $this->regexReplace("^[$chars]+|[$chars]+\$", '');
305
  }
306
307
  /**
308
   * Replaces all occurrences of $pattern in $str by $replacement.
309
   *
310
   * @param  string $pattern     <p>The regular expression pattern.</p>
311
   * @param  string $replacement <p>The string to replace with.</p>
312
   * @param  string $options     [optional] <p>Matching conditions to be used.</p>
313
   * @param  string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
314
   *
315
   * @return static <p>Object with the result2ing $str after the replacements.</p>
316
   */
317 259
  public function regexReplace(string $pattern, string $replacement, string $options = '', string $delimiter = '/'): self
318
  {
319 259
    if ($options === 'msr') {
320 9
      $options = 'ms';
321
    }
322
323
    // fallback
324 259
    if (!$delimiter) {
325
      $delimiter = '/';
326
    }
327
328 259
    $str = (string)\preg_replace(
329 259
        $delimiter . $pattern . $delimiter . 'u' . $options,
330 259
        $replacement,
331 259
        $this->str
332
    );
333
334 259
    return static::create($str, $this->encoding);
335
  }
336
337
  /**
338
   * Returns true if the string contains all $needles, false otherwise. By
339
   * default the comparison is case-sensitive, but can be made insensitive by
340
   * setting $caseSensitive to false.
341
   *
342
   * @param  array $needles       <p>SubStrings to look for.</p>
343
   * @param  bool  $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
344
   *
345
   * @return bool  <p>Whether or not $str contains $needle.</p>
346
   */
347 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...
348
  {
349
    /** @noinspection IsEmptyFunctionUsageInspection */
350 43
    if (empty($needles)) {
351 1
      return false;
352
    }
353
354 42
    foreach ($needles as $needle) {
355 42
      if (!$this->contains($needle, $caseSensitive)) {
356 42
        return false;
357
      }
358
    }
359
360 24
    return true;
361
  }
362
363
  /**
364
   * Returns true if the string contains $needle, false otherwise. By default
365
   * the comparison is case-sensitive, but can be made insensitive by setting
366
   * $caseSensitive to false.
367
   *
368
   * @param string $needle        <p>Substring to look for.</p>
369
   * @param bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
370
   *
371
   * @return bool   <p>Whether or not $str contains $needle.</p>
372
   */
373 105
  public function contains(string $needle, bool $caseSensitive = true): bool
374
  {
375 105
    $encoding = $this->encoding;
376
377 105
    if ($caseSensitive) {
378 55
      return (UTF8::strpos($this->str, $needle, 0, $encoding) !== false);
379
    }
380
381 50
    return (UTF8::stripos($this->str, $needle, 0, $encoding) !== false);
382
  }
383
384
  /**
385
   * Returns true if the string contains any $needles, false otherwise. By
386
   * default the comparison is case-sensitive, but can be made insensitive by
387
   * setting $caseSensitive to false.
388
   *
389
   * @param  array $needles       <p>SubStrings to look for.</p>
390
   * @param  bool  $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
391
   *
392
   * @return bool <p>Whether or not $str contains $needle.</p>
393
   */
394 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...
395
  {
396
    /** @noinspection IsEmptyFunctionUsageInspection */
397 43
    if (empty($needles)) {
398 1
      return false;
399
    }
400
401 42
    foreach ($needles as $needle) {
402 42
      if ($this->contains($needle, $caseSensitive)) {
403 42
        return true;
404
      }
405
    }
406
407 18
    return false;
408
  }
409
410
  /**
411
   * Returns the length of the string, implementing the countable interface.
412
   *
413
   * @return int <p>The number of characters in the string, given the encoding.</p>
414
   */
415 1
  public function count(): int
416
  {
417 1
    return $this->length();
418
  }
419
420
  /**
421
   * Returns the number of occurrences of $substring in the given string.
422
   * By default, the comparison is case-sensitive, but can be made insensitive
423
   * by setting $caseSensitive to false.
424
   *
425
   * @param  string $substring     <p>The substring to search for.</p>
426
   * @param  bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
427
   *
428
   * @return int|false <p>This functions returns an integer or false if there isn't a string.</p>
429
   */
430 15
  public function countSubstr(string $substring, bool $caseSensitive = true)
431
  {
432 15
    if ($caseSensitive) {
433 9
      return UTF8::substr_count($this->str, $substring, 0, null, $this->encoding);
434
    }
435
436 6
    $str = UTF8::strtoupper($this->str, $this->encoding);
437 6
    $substring = UTF8::strtoupper($substring, $this->encoding);
438
439 6
    return UTF8::substr_count($str, $substring, 0, null, $this->encoding);
440
  }
441
442
  /**
443
   * Returns a lowercase and trimmed string separated by dashes. Dashes are
444
   * inserted before uppercase characters (with the exception of the first
445
   * character of the string), and in place of spaces as well as underscores.
446
   *
447
   * @return static <p>Object with a dasherized $str</p>
448
   */
449 19
  public function dasherize(): self
450
  {
451 19
    return $this->delimit('-');
452
  }
453
454
  /**
455
   * Returns a lowercase and trimmed string separated by the given delimiter.
456
   * Delimiters are inserted before uppercase characters (with the exception
457
   * of the first character of the string), and in place of spaces, dashes,
458
   * and underscores. Alpha delimiters are not converted to lowercase.
459
   *
460
   * @param string $delimiter <p>Sequence used to separate parts of the string.</p>
461
   *
462
   * @return static <p>Object with a delimited $str.</p>
463
   */
464 49
  public function delimit(string $delimiter): self
465
  {
466 49
    $str = $this->trim();
467
468 49
    $str = (string)\preg_replace('/\B([A-Z])/u', '-\1', $str);
469
470 49
    $str = UTF8::strtolower($str, $this->encoding);
471
472 49
    $str = (string)\preg_replace('/[-_\s]+/u', $delimiter, $str);
473
474 49
    return static::create($str, $this->encoding);
475
  }
476
477
  /**
478
   * Ensures that the string begins with $substring. If it doesn't, it's
479
   * prepended.
480
   *
481
   * @param string $substring <p>The substring to add if not present.</p>
482
   *
483
   * @return static <p>Object with its $str prefixed by the $substring.</p>
484
   */
485 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...
486
  {
487 10
    $stringy = static::create($this->str, $this->encoding);
488
489 10
    if (!$stringy->startsWith($substring)) {
490 4
      $stringy->str = $substring . $stringy->str;
491
    }
492
493 10
    return $stringy;
494
  }
495
496
  /**
497
   * Returns true if the string begins with $substring, false otherwise. By
498
   * default, the comparison is case-sensitive, but can be made insensitive
499
   * by setting $caseSensitive to false.
500
   *
501
   * @param  string $substring     <p>The substring to look for.</p>
502
   * @param  bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
503
   *
504
   * @return bool   <p>Whether or not $str starts with $substring.</p>
505
   */
506 45
  public function startsWith(string $substring, bool $caseSensitive = true): bool
507
  {
508 45
    $str = $this->str;
509
510 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...
511 8
      $substring = UTF8::strtolower($substring, $this->encoding);
512 8
      $str = UTF8::strtolower($this->str, $this->encoding);
513
    }
514
515 45
    return UTF8::strpos($str, $substring, 0, $this->encoding) === 0;
516
  }
517
518
  /**
519
   * Returns true if the string begins with any of $substrings, false otherwise.
520
   * By default the comparison is case-sensitive, but can be made insensitive by
521
   * setting $caseSensitive to false.
522
   *
523
   * @param  array $substrings    <p>Substrings to look for.</p>
524
   * @param  bool  $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
525
   *
526
   * @return bool  <p>Whether or not $str starts with $substring.</p>
527
   */
528 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...
529
  {
530 12
    if (empty($substrings)) {
531
      return false;
532
    }
533
534 12
    foreach ($substrings as $substring) {
535 12
      if ($this->startsWith($substring, $caseSensitive)) {
536 12
        return true;
537
      }
538
    }
539
540 6
    return false;
541
  }
542
543
  /**
544
   * Ensures that the string ends with $substring. If it doesn't, it's appended.
545
   *
546
   * @param string $substring <p>The substring to add if not present.</p>
547
   *
548
   * @return static <p>Object with its $str suffixed by the $substring.</p>
549
   */
550 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...
551
  {
552 10
    $stringy = static::create($this->str, $this->encoding);
553
554 10
    if (!$stringy->endsWith($substring)) {
555 4
      $stringy->str .= $substring;
556
    }
557
558 10
    return $stringy;
559
  }
560
561
  /**
562
   * Returns true if the string ends with $substring, false otherwise. By
563
   * default, the comparison is case-sensitive, but can be made insensitive
564
   * by setting $caseSensitive to false.
565
   *
566
   * @param string $substring     <p>The substring to look for.</p>
567
   * @param bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
568
   *
569
   * @return bool   <p>Whether or not $str ends with $substring.</p>
570
   */
571 44
  public function endsWith(string $substring, bool $caseSensitive = true): bool
572
  {
573 44
    $substringLength = UTF8::strlen($substring, $this->encoding);
574 44
    $strLength = $this->length();
575
576 44
    $endOfStr = UTF8::substr(
577 44
        $this->str,
578 44
        $strLength - $substringLength,
579 44
        $substringLength,
580 44
        $this->encoding
581
    );
582
583 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...
584 8
      $substring = UTF8::strtolower($substring, $this->encoding);
585 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...
586
    }
587
588 44
    return $substring === $endOfStr;
589
  }
590
591
  /**
592
   * Returns true if the string ends with any of $substrings, false otherwise.
593
   * By default, the comparison is case-sensitive, but can be made insensitive
594
   * by setting $caseSensitive to false.
595
   *
596
   * @param string[] $substrings    <p>Substrings to look for.</p>
597
   * @param bool     $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
598
   *
599
   * @return bool     <p>Whether or not $str ends with $substring.</p>
600
   */
601 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...
602
  {
603 11
    if (empty($substrings)) {
604
      return false;
605
    }
606
607 11
    foreach ($substrings as $substring) {
608 11
      if ($this->endsWith($substring, $caseSensitive)) {
609 11
        return true;
610
      }
611
    }
612
613 6
    return false;
614
  }
615
616
  /**
617
   * Returns the first $n characters of the string.
618
   *
619
   * @param int $n <p>Number of characters to retrieve from the start.</p>
620
   *
621
   * @return static <p>Object with its $str being the first $n chars.</p>
622
   */
623 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...
624
  {
625 13
    $stringy = static::create($this->str, $this->encoding);
626
627 13
    if ($n < 0) {
628 2
      $stringy->str = '';
629
    } else {
630 11
      return $stringy->substr(0, $n);
631
    }
632
633 2
    return $stringy;
634
  }
635
636
  /**
637
   * Returns the encoding used by the Stringy object.
638
   *
639
   * @return string <p>The current value of the $encoding property.</p>
640
   */
641 3
  public function getEncoding(): string
642
  {
643 3
    return $this->encoding;
644
  }
645
646
  /**
647
   * Returns a new ArrayIterator, thus implementing the IteratorAggregate
648
   * interface. The ArrayIterator's constructor is passed an array of chars
649
   * in the multibyte string. This enables the use of foreach with instances
650
   * of Stringy\Stringy.
651
   *
652
   * @return \ArrayIterator <p>An iterator for the characters in the string.</p>
653
   */
654 1
  public function getIterator(): \ArrayIterator
655
  {
656 1
    return new \ArrayIterator($this->chars());
657
  }
658
659
  /**
660
   * Returns an array consisting of the characters in the string.
661
   *
662
   * @return array <p>An array of string chars.</p>
663
   */
664 4
  public function chars(): array
665
  {
666
    // init
667 4
    $chars = [];
668 4
    $l = $this->length();
669
670 4
    for ($i = 0; $i < $l; $i++) {
671 3
      $chars[] = $this->at($i)->str;
672
    }
673
674 4
    return $chars;
675
  }
676
677
  /**
678
   * Returns the character at $index, with indexes starting at 0.
679
   *
680
   * @param int $index <p>Position of the character.</p>
681
   *
682
   * @return static <p>The character at $index.</p>
683
   */
684 11
  public function at(int $index): self
685
  {
686 11
    return $this->substr($index, 1);
687
  }
688
689
  /**
690
   * Returns true if the string contains a lower case char, false otherwise.
691
   *
692
   * @return bool <p>Whether or not the string contains a lower case character.</p>
693
   */
694 12
  public function hasLowerCase(): bool
695
  {
696 12
    return $this->matchesPattern('.*[[:lower:]]');
697
  }
698
699
  /**
700
   * Returns true if $str matches the supplied pattern, false otherwise.
701
   *
702
   * @param string $pattern <p>Regex pattern to match against.</p>
703
   *
704
   * @return bool <p>Whether or not $str matches the pattern.</p>
705
   */
706 103
  protected function matchesPattern(string $pattern): bool
707
  {
708 103
    if (\preg_match('/' . $pattern . '/u', $this->str)) {
709 64
      return true;
710
    }
711
712 39
    return false;
713
  }
714
715
  /**
716
   * Returns true if the string contains an upper case char, false otherwise.
717
   *
718
   * @return bool <p>Whether or not the string contains an upper case character.</p>
719
   */
720 12
  public function hasUpperCase(): bool
721
  {
722 12
    return $this->matchesPattern('.*[[:upper:]]');
723
  }
724
725
  /**
726
   * Convert all HTML entities to their applicable characters.
727
   *
728
   * @param int $flags       [optional] <p>
729
   *                         A bitmask of one or more of the following flags, which specify how to handle quotes and
730
   *                         which document type to use. The default is ENT_COMPAT.
731
   *                         <table>
732
   *                         Available <i>flags</i> constants
733
   *                         <tr valign="top">
734
   *                         <td>Constant Name</td>
735
   *                         <td>Description</td>
736
   *                         </tr>
737
   *                         <tr valign="top">
738
   *                         <td><b>ENT_COMPAT</b></td>
739
   *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
740
   *                         </tr>
741
   *                         <tr valign="top">
742
   *                         <td><b>ENT_QUOTES</b></td>
743
   *                         <td>Will convert both double and single quotes.</td>
744
   *                         </tr>
745
   *                         <tr valign="top">
746
   *                         <td><b>ENT_NOQUOTES</b></td>
747
   *                         <td>Will leave both double and single quotes unconverted.</td>
748
   *                         </tr>
749
   *                         <tr valign="top">
750
   *                         <td><b>ENT_HTML401</b></td>
751
   *                         <td>
752
   *                         Handle code as HTML 4.01.
753
   *                         </td>
754
   *                         </tr>
755
   *                         <tr valign="top">
756
   *                         <td><b>ENT_XML1</b></td>
757
   *                         <td>
758
   *                         Handle code as XML 1.
759
   *                         </td>
760
   *                         </tr>
761
   *                         <tr valign="top">
762
   *                         <td><b>ENT_XHTML</b></td>
763
   *                         <td>
764
   *                         Handle code as XHTML.
765
   *                         </td>
766
   *                         </tr>
767
   *                         <tr valign="top">
768
   *                         <td><b>ENT_HTML5</b></td>
769
   *                         <td>
770
   *                         Handle code as HTML 5.
771
   *                         </td>
772
   *                         </tr>
773
   *                         </table>
774
   *                         </p>
775
   *
776
   * @return static <p>Object with the resulting $str after being html decoded.</p>
777
   */
778 5
  public function htmlDecode(int $flags = ENT_COMPAT): self
779
  {
780 5
    $str = UTF8::html_entity_decode($this->str, $flags, $this->encoding);
781
782 5
    return static::create($str, $this->encoding);
783
  }
784
785
  /**
786
   * Convert all applicable characters to HTML entities.
787
   *
788
   * @param int $flags       [optional] <p>
789
   *                         A bitmask of one or more of the following flags, which specify how to handle quotes and
790
   *                         which document type to use. The default is ENT_COMPAT.
791
   *                         <table>
792
   *                         Available <i>flags</i> constants
793
   *                         <tr valign="top">
794
   *                         <td>Constant Name</td>
795
   *                         <td>Description</td>
796
   *                         </tr>
797
   *                         <tr valign="top">
798
   *                         <td><b>ENT_COMPAT</b></td>
799
   *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
800
   *                         </tr>
801
   *                         <tr valign="top">
802
   *                         <td><b>ENT_QUOTES</b></td>
803
   *                         <td>Will convert both double and single quotes.</td>
804
   *                         </tr>
805
   *                         <tr valign="top">
806
   *                         <td><b>ENT_NOQUOTES</b></td>
807
   *                         <td>Will leave both double and single quotes unconverted.</td>
808
   *                         </tr>
809
   *                         <tr valign="top">
810
   *                         <td><b>ENT_HTML401</b></td>
811
   *                         <td>
812
   *                         Handle code as HTML 4.01.
813
   *                         </td>
814
   *                         </tr>
815
   *                         <tr valign="top">
816
   *                         <td><b>ENT_XML1</b></td>
817
   *                         <td>
818
   *                         Handle code as XML 1.
819
   *                         </td>
820
   *                         </tr>
821
   *                         <tr valign="top">
822
   *                         <td><b>ENT_XHTML</b></td>
823
   *                         <td>
824
   *                         Handle code as XHTML.
825
   *                         </td>
826
   *                         </tr>
827
   *                         <tr valign="top">
828
   *                         <td><b>ENT_HTML5</b></td>
829
   *                         <td>
830
   *                         Handle code as HTML 5.
831
   *                         </td>
832
   *                         </tr>
833
   *                         </table>
834
   *                         </p>
835
   *
836
   * @return static <p>Object with the resulting $str after being html encoded.</p>
837
   */
838 5
  public function htmlEncode(int $flags = ENT_COMPAT): self
839
  {
840 5
    $str = UTF8::htmlentities($this->str, $flags, $this->encoding);
841
842 5
    return static::create($str, $this->encoding);
843
  }
844
845
  /**
846
   * Capitalizes the first word of the string, replaces underscores with
847
   * spaces, and strips '_id'.
848
   *
849
   * @return static <p>Object with a humanized $str.</p>
850
   */
851 3
  public function humanize(): self
852
  {
853 3
    $str = UTF8::str_replace(['_id', '_'], ['', ' '], $this->str);
854
855 3
    return static::create($str, $this->encoding)->trim()->upperCaseFirst();
856
  }
857
858
  /**
859
   * Converts the first character of the supplied string to upper case.
860
   *
861
   * @return static <p>Object with the first character of $str being upper case.</p>
862
   */
863 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...
864
  {
865 61
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
866 61
    $rest = UTF8::substr(
867 61
        $this->str,
868 61
        1,
869 61
        $this->length() - 1,
870 61
        $this->encoding
871
    );
872
873 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 865 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...
874
875 61
    return static::create($str, $this->encoding);
876
  }
877
878
  /**
879
   * Returns the index of the last occurrence of $needle in the string,
880
   * and false if not found. Accepts an optional offset from which to begin
881
   * the search. Offsets may be negative to count from the last character
882
   * in the string.
883
   *
884
   * @param  string $needle <p>Substring to look for.</p>
885
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
886
   *
887
   * @return int|false <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
888
   */
889 12
  public function indexOfLast(string $needle, int $offset = 0)
890
  {
891 12
    return UTF8::strrpos($this->str, $needle, $offset, $this->encoding);
892
  }
893
894
  /**
895
   * Returns the index of the last occurrence of $needle in the string,
896
   * and false if not found. Accepts an optional offset from which to begin
897
   * the search. Offsets may be negative to count from the last character
898
   * in the string.
899
   *
900
   * @param  string $needle <p>Substring to look for.</p>
901
   * @param  int    $offset [optional] <p>Offset from which to search. Default: 0</p>
902
   *
903
   * @return int|false <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
904
   */
905 2
  public function indexOfLastIgnoreCase(string $needle, int $offset = 0)
906
  {
907 2
    return UTF8::strripos($this->str, $needle, $offset, $this->encoding);
908
  }
909
910
  /**
911
   * Inserts $substring into the string at the $index provided.
912
   *
913
   * @param  string $substring <p>String to be inserted.</p>
914
   * @param  int    $index     <p>The index at which to insert the substring.</p>
915
   *
916
   * @return static <p>Object with the resulting $str after the insertion.</p>
917
   */
918 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...
919
  {
920 8
    $stringy = static::create($this->str, $this->encoding);
921 8
    if ($index > $stringy->length()) {
922 1
      return $stringy;
923
    }
924
925 7
    $start = UTF8::substr($stringy->str, 0, $index, $stringy->encoding);
926 7
    $end = UTF8::substr($stringy->str, $index, $stringy->length(), $stringy->encoding);
927
928 7
    $stringy->str = $start . $substring . $end;
929
930 7
    return $stringy;
931
  }
932
933
  /**
934
   * Returns true if the string contains the $pattern, otherwise false.
935
   *
936
   * WARNING: Asterisks ("*") are translated into (".*") zero-or-more regular
937
   * expression wildcards.
938
   *
939
   * @credit Originally from Laravel, thanks Taylor.
940
   *
941
   * @param string $pattern <p>The string or pattern to match against.</p>
942
   *
943
   * @return bool <p>Whether or not we match the provided pattern.</p>
944
   */
945 13
  public function is(string $pattern): bool
946
  {
947 13
    if ($this->toString() === $pattern) {
948 1
      return true;
949
    }
950
951 12
    $quotedPattern = \preg_quote($pattern, '/');
952 12
    $replaceWildCards = \str_replace('\*', '.*', $quotedPattern);
953
954 12
    return $this->matchesPattern('^' . $replaceWildCards . '\z');
955
  }
956
957
  /**
958
   * Returns true if the string contains only alphabetic chars, false otherwise.
959
   *
960
   * @return bool <p>Whether or not $str contains only alphabetic chars.</p>
961
   */
962 10
  public function isAlpha(): bool
963
  {
964 10
    return $this->matchesPattern('^[[:alpha:]]*$');
965
  }
966
967
  /**
968
   * Determine whether the string is considered to be empty.
969
   *
970
   * A variable is considered empty if it does not exist or if its value equals FALSE.
971
   * empty() does not generate a warning if the variable does not exist.
972
   *
973
   * @return bool <p>Whether or not $str is empty().</p>
974
   */
975
  public function isEmpty(): bool
976
  {
977
    return empty($this->str);
978
  }
979
980
  /**
981
   * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
982
   *
983
   * @return bool <p>Whether or not $str contains only alphanumeric chars.</p>
984
   */
985 13
  public function isAlphanumeric(): bool
986
  {
987 13
    return $this->matchesPattern('^[[:alnum:]]*$');
988
  }
989
990
  /**
991
   * Returns true if the string contains only whitespace chars, false otherwise.
992
   *
993
   * @return bool <p>Whether or not $str contains only whitespace characters.</p>
994
   */
995 15
  public function isBlank(): bool
996
  {
997 15
    return $this->matchesPattern('^[[:space:]]*$');
998
  }
999
1000
  /**
1001
   * Returns true if the string contains only hexadecimal chars, false otherwise.
1002
   *
1003
   * @return bool <p>Whether or not $str contains only hexadecimal chars.</p>
1004
   */
1005 13
  public function isHexadecimal(): bool
1006
  {
1007 13
    return $this->matchesPattern('^[[:xdigit:]]*$');
1008
  }
1009
1010
  /**
1011
   * Returns true if the string contains HTML-Tags, false otherwise.
1012
   *
1013
   * @return bool <p>Whether or not $str contains HTML-Tags.</p>
1014
   */
1015 1
  public function isHtml(): bool
1016
  {
1017 1
    return UTF8::is_html($this->str);
1018
  }
1019
1020
  /**
1021
   * Returns true if the string contains a valid E-Mail address, false otherwise.
1022
   *
1023
   * @param bool $useExampleDomainCheck   [optional] <p>Default: false</p>
1024
   * @param bool $useTypoInDomainCheck    [optional] <p>Default: false</p>
1025
   * @param bool $useTemporaryDomainCheck [optional] <p>Default: false</p>
1026
   * @param bool $useDnsCheck             [optional] <p>Default: false</p>
1027
   *
1028
   * @return bool <p>Whether or not $str contains a valid E-Mail address.</p>
1029
   */
1030 1
  public function isEmail(bool $useExampleDomainCheck = false, bool $useTypoInDomainCheck = false, bool $useTemporaryDomainCheck = false, bool $useDnsCheck = false): bool
1031
  {
1032 1
    return EmailCheck::isValid($this->str, $useExampleDomainCheck, $useTypoInDomainCheck, $useTemporaryDomainCheck, $useDnsCheck);
1033
  }
1034
1035
  /**
1036
   * Returns true if the string is JSON, false otherwise. Unlike json_decode
1037
   * in PHP 5.x, this method is consistent with PHP 7 and other JSON parsers,
1038
   * in that an empty string is not considered valid JSON.
1039
   *
1040
   * @return bool <p>Whether or not $str is JSON.</p>
1041
   */
1042 20
  public function isJson(): bool
1043
  {
1044 20
    if (!isset($this->str[0])) {
1045 1
      return false;
1046
    }
1047
1048 19
    \json_decode($this->str);
1049
1050 19
    return \json_last_error() === JSON_ERROR_NONE;
1051
  }
1052
1053
  /**
1054
   * Returns true if the string contains only lower case chars, false otherwise.
1055
   *
1056
   * @return bool <p>Whether or not $str contains only lower case characters.</p>
1057
   */
1058 8
  public function isLowerCase(): bool
1059
  {
1060 8
    if ($this->matchesPattern('^[[:lower:]]*$')) {
1061 3
      return true;
1062
    }
1063
1064 5
    return false;
1065
  }
1066
1067
  /**
1068
   * Returns true if the string is serialized, false otherwise.
1069
   *
1070
   * @return bool <p>Whether or not $str is serialized.</p>
1071
   */
1072 7
  public function isSerialized(): bool
1073
  {
1074 7
    if (!isset($this->str[0])) {
1075 1
      return false;
1076
    }
1077
1078
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
1079
    /** @noinspection UnserializeExploitsInspection */
1080 6
    return $this->str === 'b:0;'
1081
           ||
1082 6
           @\unserialize($this->str) !== false;
1083
  }
1084
1085
  /**
1086
   * Returns true if the string contains only lower case chars, false
1087
   * otherwise.
1088
   *
1089
   * @return bool <p>Whether or not $str contains only lower case characters.</p>
1090
   */
1091 8
  public function isUpperCase(): bool
1092
  {
1093 8
    return $this->matchesPattern('^[[:upper:]]*$');
1094
  }
1095
1096
  /**
1097
   * Returns the last $n characters of the string.
1098
   *
1099
   * @param int $n <p>Number of characters to retrieve from the end.</p>
1100
   *
1101
   * @return static <p>Object with its $str being the last $n chars.</p>
1102
   */
1103 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...
1104
  {
1105 12
    $stringy = static::create($this->str, $this->encoding);
1106
1107 12
    if ($n <= 0) {
1108 4
      $stringy->str = '';
1109
    } else {
1110 8
      return $stringy->substr(-$n);
1111
    }
1112
1113 4
    return $stringy;
1114
  }
1115
1116
  /**
1117
   * Splits on newlines and carriage returns, returning an array of Stringy
1118
   * objects corresponding to the lines in the string.
1119
   *
1120
   * @return static[] <p>An array of Stringy objects.</p>
1121
   */
1122 15
  public function lines(): array
1123
  {
1124 15
    $array = \preg_split('/[\r\n]{1,2}/u', $this->str);
1125
    /** @noinspection CallableInLoopTerminationConditionInspection */
1126
    /** @noinspection ForeachInvariantsInspection */
1127 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...
1128 15
      $array[$i] = static::create($array[$i], $this->encoding);
1129
    }
1130
1131 15
    return $array;
1132
  }
1133
1134
  /**
1135
   * Returns the longest common prefix between the string and $otherStr.
1136
   *
1137
   * @param string $otherStr <p>Second string for comparison.</p>
1138
   *
1139
   * @return static <p>Object with its $str being the longest common prefix.</p>
1140
   */
1141 10
  public function longestCommonPrefix(string $otherStr): self
1142
  {
1143 10
    $encoding = $this->encoding;
1144 10
    $maxLength = \min($this->length(), UTF8::strlen($otherStr, $encoding));
1145
1146 10
    $longestCommonPrefix = '';
1147 10
    for ($i = 0; $i < $maxLength; $i++) {
1148 8
      $char = UTF8::substr($this->str, $i, 1, $encoding);
1149
1150 8
      if ($char == UTF8::substr($otherStr, $i, 1, $encoding)) {
1151 6
        $longestCommonPrefix .= $char;
1152
      } else {
1153 6
        break;
1154
      }
1155
    }
1156
1157 10
    return static::create($longestCommonPrefix, $encoding);
1158
  }
1159
1160
  /**
1161
   * Returns the longest common suffix between the string and $otherStr.
1162
   *
1163
   * @param string $otherStr <p>Second string for comparison.</p>
1164
   *
1165
   * @return static <p>Object with its $str being the longest common suffix.</p>
1166
   */
1167 10
  public function longestCommonSuffix(string $otherStr): self
1168
  {
1169 10
    $encoding = $this->encoding;
1170 10
    $maxLength = \min($this->length(), UTF8::strlen($otherStr, $encoding));
1171
1172 10
    $longestCommonSuffix = '';
1173 10
    for ($i = 1; $i <= $maxLength; $i++) {
1174 8
      $char = UTF8::substr($this->str, -$i, 1, $encoding);
1175
1176 8
      if ($char == UTF8::substr($otherStr, -$i, 1, $encoding)) {
1177 6
        $longestCommonSuffix = $char . $longestCommonSuffix;
1178
      } else {
1179 6
        break;
1180
      }
1181
    }
1182
1183 10
    return static::create($longestCommonSuffix, $encoding);
1184
  }
1185
1186
  /**
1187
   * Returns the longest common substring between the string and $otherStr.
1188
   * In the case of ties, it returns that which occurs first.
1189
   *
1190
   * @param string $otherStr <p>Second string for comparison.</p>
1191
   *
1192
   * @return static <p>Object with its $str being the longest common substring.</p>
1193
   */
1194 10
  public function longestCommonSubstring(string $otherStr): self
1195
  {
1196
    // Uses dynamic programming to solve
1197
    // http://en.wikipedia.org/wiki/Longest_common_substring_problem
1198 10
    $encoding = $this->encoding;
1199 10
    $stringy = static::create($this->str, $encoding);
1200 10
    $strLength = $stringy->length();
1201 10
    $otherLength = UTF8::strlen($otherStr, $encoding);
1202
1203
    // Return if either string is empty
1204 10
    if ($strLength == 0 || $otherLength == 0) {
1205 2
      $stringy->str = '';
1206
1207 2
      return $stringy;
1208
    }
1209
1210 8
    $len = 0;
1211 8
    $end = 0;
1212 8
    $table = \array_fill(
1213 8
        0,
1214 8
        $strLength + 1,
1215 8
        \array_fill(0, $otherLength + 1, 0)
1216
    );
1217
1218 8
    for ($i = 1; $i <= $strLength; $i++) {
1219 8
      for ($j = 1; $j <= $otherLength; $j++) {
1220 8
        $strChar = UTF8::substr($stringy->str, $i - 1, 1, $encoding);
1221 8
        $otherChar = UTF8::substr($otherStr, $j - 1, 1, $encoding);
1222
1223 8
        if ($strChar == $otherChar) {
1224 8
          $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
1225 8
          if ($table[$i][$j] > $len) {
1226 8
            $len = $table[$i][$j];
1227 8
            $end = $i;
1228
          }
1229
        } else {
1230 8
          $table[$i][$j] = 0;
1231
        }
1232
      }
1233
    }
1234
1235 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...
1236
1237 8
    return $stringy;
1238
  }
1239
1240
  /**
1241
   * Returns whether or not a character exists at an index. Offsets may be
1242
   * negative to count from the last character in the string. Implements
1243
   * part of the ArrayAccess interface.
1244
   *
1245
   * @param int $offset <p>The index to check.</p>
1246
   *
1247
   * @return boolean <p>Whether or not the index exists.</p>
1248
   */
1249 6
  public function offsetExists($offset): bool
1250
  {
1251
    // init
1252 6
    $length = $this->length();
1253 6
    $offset = (int)$offset;
1254
1255 6
    if ($offset >= 0) {
1256 3
      return ($length > $offset);
1257
    }
1258
1259 3
    return ($length >= \abs($offset));
1260
  }
1261
1262
  /**
1263
   * Returns the character at the given index. Offsets may be negative to
1264
   * count from the last character in the string. Implements part of the
1265
   * ArrayAccess interface, and throws an OutOfBoundsException if the index
1266
   * does not exist.
1267
   *
1268
   * @param int $offset <p>The <strong>index</strong> from which to retrieve the char.</p>
1269
   *
1270
   * @return string <p>The character at the specified index.</p>
1271
   *
1272
   * @throws \OutOfBoundsException <p>If the positive or negative offset does not exist.</p>
1273
   */
1274 2
  public function offsetGet($offset): string
1275
  {
1276
    // init
1277 2
    $offset = (int)$offset;
1278 2
    $length = $this->length();
1279
1280
    if (
1281 2
        ($offset >= 0 && $length <= $offset)
1282
        ||
1283 2
        $length < \abs($offset)
1284
    ) {
1285 1
      throw new \OutOfBoundsException('No character exists at the index');
1286
    }
1287
1288 1
    return UTF8::substr($this->str, $offset, 1, $this->encoding);
1289
  }
1290
1291
  /**
1292
   * Implements part of the ArrayAccess interface, but throws an exception
1293
   * when called. This maintains the immutability of Stringy objects.
1294
   *
1295
   * @param int   $offset <p>The index of the character.</p>
1296
   * @param mixed $value  <p>Value to set.</p>
1297
   *
1298
   * @throws \Exception <p>When called.</p>
1299
   */
1300 1
  public function offsetSet($offset, $value)
1301
  {
1302
    // Stringy is immutable, cannot directly set char
1303
    /** @noinspection ThrowRawExceptionInspection */
1304 1
    throw new \Exception('Stringy object is immutable, cannot modify char');
1305
  }
1306
1307
  /**
1308
   * Implements part of the ArrayAccess interface, but throws an exception
1309
   * when called. This maintains the immutability of Stringy objects.
1310
   *
1311
   * @param int $offset <p>The index of the character.</p>
1312
   *
1313
   * @throws \Exception <p>When called.</p>
1314
   */
1315 1
  public function offsetUnset($offset)
1316
  {
1317
    // Don't allow directly modifying the string
1318
    /** @noinspection ThrowRawExceptionInspection */
1319 1
    throw new \Exception('Stringy object is immutable, cannot unset char');
1320
  }
1321
1322
  /**
1323
   * Pads the string to a given length with $padStr. If length is less than
1324
   * or equal to the length of the string, no padding takes places. The
1325
   * default string used for padding is a space, and the default type (one of
1326
   * 'left', 'right', 'both') is 'right'. Throws an InvalidArgumentException
1327
   * if $padType isn't one of those 3 values.
1328
   *
1329
   * @param int    $length  <p>Desired string length after padding.</p>
1330
   * @param string $padStr  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1331
   * @param string $padType [optional] <p>One of 'left', 'right', 'both'. Default: 'right'</p>
1332
   *
1333
   * @return static <p>Object with a padded $str.</p>
1334
   *
1335
   * @throws \InvalidArgumentException <p>If $padType isn't one of 'right', 'left' or 'both'.</p>
1336
   */
1337 13
  public function pad(int $length, string $padStr = ' ', string $padType = 'right'): self
1338
  {
1339 13
    if (!\in_array($padType, ['left', 'right', 'both'], true)) {
1340 1
      throw new \InvalidArgumentException(
1341 1
          'Pad expects $padType ' . "to be one of 'left', 'right' or 'both'"
1342
      );
1343
    }
1344
1345 12
    switch ($padType) {
1346
      case 'left':
1347 3
        return $this->padLeft($length, $padStr);
1348
      case 'right':
1349 6
        return $this->padRight($length, $padStr);
1350
      default:
1351 3
        return $this->padBoth($length, $padStr);
1352
    }
1353
  }
1354
1355
  /**
1356
   * Returns a new string of a given length such that the beginning of the
1357
   * string is padded. Alias for pad() with a $padType of 'left'.
1358
   *
1359
   * @param int    $length <p>Desired string length after padding.</p>
1360
   * @param string $padStr [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1361
   *
1362
   * @return static <p>String with left padding.</p>
1363
   */
1364 10
  public function padLeft(int $length, string $padStr = ' '): self
1365
  {
1366 10
    return $this->applyPadding($length - $this->length(), 0, $padStr);
1367
  }
1368
1369
  /**
1370
   * Adds the specified amount of left and right padding to the given string.
1371
   * The default character used is a space.
1372
   *
1373
   * @param int    $left   [optional] <p>Length of left padding. Default: 0</p>
1374
   * @param int    $right  [optional] <p>Length of right padding. Default: 0</p>
1375
   * @param string $padStr [optional] <p>String used to pad. Default: ' '</p>
1376
   *
1377
   * @return static <p>String with padding applied.</p>
1378
   */
1379 37
  protected function applyPadding(int $left = 0, int $right = 0, string $padStr = ' '): self
1380
  {
1381 37
    $stringy = static::create($this->str, $this->encoding);
1382
1383 37
    $length = UTF8::strlen($padStr, $stringy->encoding);
1384
1385 37
    $strLength = $stringy->length();
1386 37
    $paddedLength = $strLength + $left + $right;
1387
1388 37
    if (!$length || $paddedLength <= $strLength) {
1389 3
      return $stringy;
1390
    }
1391
1392 34
    $leftPadding = UTF8::substr(
1393 34
        UTF8::str_repeat(
1394 34
            $padStr,
1395 34
            (int)\ceil($left / $length)
1396
        ),
1397 34
        0,
1398 34
        $left,
1399 34
        $stringy->encoding
1400
    );
1401
1402 34
    $rightPadding = UTF8::substr(
1403 34
        UTF8::str_repeat(
1404 34
            $padStr,
1405 34
            (int)\ceil($right / $length)
1406
        ),
1407 34
        0,
1408 34
        $right,
1409 34
        $stringy->encoding
1410
    );
1411
1412 34
    $stringy->str = $leftPadding . $stringy->str . $rightPadding;
1413
1414 34
    return $stringy;
1415
  }
1416
1417
  /**
1418
   * Returns a new string of a given length such that the end of the string
1419
   * is padded. Alias for pad() with a $padType of 'right'.
1420
   *
1421
   * @param int    $length <p>Desired string length after padding.</p>
1422
   * @param string $padStr [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1423
   *
1424
   * @return static <p>String with right padding.</p>
1425
   */
1426 13
  public function padRight(int $length, string $padStr = ' '): self
1427
  {
1428 13
    return $this->applyPadding(0, $length - $this->length(), $padStr);
1429
  }
1430
1431
  /**
1432
   * Returns a new string of a given length such that both sides of the
1433
   * string are padded. Alias for pad() with a $padType of 'both'.
1434
   *
1435
   * @param int    $length <p>Desired string length after padding.</p>
1436
   * @param string $padStr [optional] <p>String used to pad, defaults to space. Default: ' '</p>
1437
   *
1438
   * @return static <p>String with padding applied.</p>
1439
   */
1440 14
  public function padBoth(int $length, string $padStr = ' '): self
1441
  {
1442 14
    $padding = $length - $this->length();
1443
1444 14
    return $this->applyPadding((int)\floor($padding / 2), (int)\ceil($padding / 2), $padStr);
1445
  }
1446
1447
  /**
1448
   * Returns a new string starting with $string.
1449
   *
1450
   * @param string $string <p>The string to append.</p>
1451
   *
1452
   * @return static <p>Object with appended $string.</p>
1453
   */
1454 2
  public function prepend(string $string): self
1455
  {
1456 2
    return static::create($string . $this->str, $this->encoding);
1457
  }
1458
1459
  /**
1460
   * Returns a new string with the prefix $substring removed, if present.
1461
   *
1462
   * @param string $substring <p>The prefix to remove.</p>
1463
   *
1464
   * @return static <p>Object having a $str without the prefix $substring.</p>
1465
   */
1466 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...
1467
  {
1468 12
    $stringy = static::create($this->str, $this->encoding);
1469
1470 12
    if ($stringy->startsWith($substring)) {
1471 6
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1472
1473 6
      return $stringy->substr($substringLength);
1474
    }
1475
1476 6
    return $stringy;
1477
  }
1478
1479
  /**
1480
   * Returns a new string with the suffix $substring removed, if present.
1481
   *
1482
   * @param string $substring <p>The suffix to remove.</p>
1483
   *
1484
   * @return static <p>Object having a $str without the suffix $substring.</p>
1485
   */
1486 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...
1487
  {
1488 12
    $stringy = static::create($this->str, $this->encoding);
1489
1490 12
    if ($stringy->endsWith($substring)) {
1491 8
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1492
1493 8
      return $stringy->substr(0, $stringy->length() - $substringLength);
1494
    }
1495
1496 4
    return $stringy;
1497
  }
1498
1499
  /**
1500
   * Returns a repeated string given a multiplier.
1501
   *
1502
   * @param int $multiplier <p>The number of times to repeat the string.</p>
1503
   *
1504
   * @return static <p>Object with a repeated str.</p>
1505
   */
1506 7
  public function repeat(int $multiplier): self
1507
  {
1508 7
    $repeated = UTF8::str_repeat($this->str, $multiplier);
1509
1510 7
    return static::create($repeated, $this->encoding);
1511
  }
1512
1513
  /**
1514
   * Replaces all occurrences of $search in $str by $replacement.
1515
   *
1516
   * @param string $search        <p>The needle to search for.</p>
1517
   * @param string $replacement   <p>The string to replace with.</p>
1518
   * @param bool   $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
1519
   *
1520
   * @return static <p>Object with the resulting $str after the replacements.</p>
1521
   */
1522 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...
1523
  {
1524 29
    if ($caseSensitive) {
1525 22
      $return = UTF8::str_replace($search, $replacement, $this->str);
1526
    } else {
1527 7
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1528
    }
1529
1530 29
    return static::create($return);
1531
  }
1532
1533
  /**
1534
   * Replaces all occurrences of $search in $str by $replacement.
1535
   *
1536
   * @param array        $search        <p>The elements to search for.</p>
1537
   * @param string|array $replacement   <p>The string to replace with.</p>
1538
   * @param bool         $caseSensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
1539
   *
1540
   * @return static <p>Object with the resulting $str after the replacements.</p>
1541
   */
1542 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...
1543
  {
1544 30
    if ($caseSensitive) {
1545 23
      $return = UTF8::str_replace($search, $replacement, $this->str);
1546
    } else {
1547 7
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1548
    }
1549
1550 30
    return static::create($return);
1551
  }
1552
1553
  /**
1554
   * Replaces all occurrences of $search from the beginning of string with $replacement.
1555
   *
1556
   * @param string $search      <p>The string to search for.</p>
1557
   * @param string $replacement <p>The replacement.</p>
1558
   *
1559
   * @return static <p>Object with the resulting $str after the replacements.</p>
1560
   */
1561 16
  public function replaceBeginning(string $search, string $replacement): self
1562
  {
1563 16
    $str = $this->regexReplace('^' . \preg_quote($search, '/'), UTF8::str_replace('\\', '\\\\', $replacement));
1564
1565 16
    return static::create($str, $this->encoding);
1566
  }
1567
1568
  /**
1569
   * Replaces all occurrences of $search from the ending of string with $replacement.
1570
   *
1571
   * @param string $search      <p>The string to search for.</p>
1572
   * @param string $replacement <p>The replacement.</p>
1573
   *
1574
   * @return static <p>Object with the resulting $str after the replacements.</p>
1575
   */
1576 16
  public function replaceEnding(string $search, string $replacement): self
1577
  {
1578 16
    $str = $this->regexReplace(\preg_quote($search, '/') . '$', UTF8::str_replace('\\', '\\\\', $replacement));
1579
1580 16
    return static::create($str, $this->encoding);
1581
  }
1582
1583
  /**
1584
   * Gets the substring after (or before via "$beforeNeedle") the first occurrence of the "$needle".
1585
   * If no match is found returns new empty Stringy object.
1586
   *
1587
   * @param string $needle       <p>The string to look for.</p>
1588
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1589
   *
1590
   * @return static
1591
   */
1592 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...
1593
  {
1594 2
    if ('' === $needle) {
1595
      return static::create();
1596
    }
1597
1598 2
    if (false === $part = UTF8::strstr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1599 2
      return static::create();
1600
    }
1601
1602 2
    return static::create($part);
1603
  }
1604
1605
  /**
1606
   * Gets the substring after (or before via "$beforeNeedle") the first occurrence of the "$needle".
1607
   * If no match is found returns new empty Stringy object.
1608
   *
1609
   * @param string $needle       <p>The string to look for.</p>
1610
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1611
   *
1612
   * @return static
1613
   */
1614 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...
1615
  {
1616 2
    if ('' === $needle) {
1617
      return static::create();
1618
    }
1619
1620 2
    if (false === $part = UTF8::stristr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1621 2
      return static::create();
1622
    }
1623
1624 2
    return static::create($part);
1625
  }
1626
1627
  /**
1628
   * Gets the substring after (or before via "$beforeNeedle") the last occurrence of the "$needle".
1629
   * If no match is found returns new empty Stringy object.
1630
   *
1631
   * @param string $needle       <p>The string to look for.</p>
1632
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1633
   *
1634
   * @return static
1635
   */
1636 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...
1637
  {
1638 2
    if ('' === $needle) {
1639
      return static::create();
1640
    }
1641
1642 2
    if (false === $part = UTF8::strrchr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1643 2
      return static::create();
1644
    }
1645
1646 2
    return static::create($part);
1647
  }
1648
1649
  /**
1650
   * Gets the substring after (or before via "$beforeNeedle") the last occurrence of the "$needle".
1651
   * If no match is found returns new empty Stringy object.
1652
   *
1653
   * @param string $needle       <p>The string to look for.</p>
1654
   * @param bool   $beforeNeedle [optional] <p>Default: false</p>
1655
   *
1656
   * @return static
1657
   */
1658 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...
1659
  {
1660 1
    if ('' === $needle) {
1661
      return static::create();
1662
    }
1663
1664 1
    if (false === $part = UTF8::strrichr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1665 1
      return static::create();
1666
    }
1667
1668 1
    return static::create($part);
1669
  }
1670
1671
  /**
1672
   * Returns a reversed string. A multibyte version of strrev().
1673
   *
1674
   * @return static <p>Object with a reversed $str.</p>
1675
   */
1676 5
  public function reverse(): self
1677
  {
1678 5
    $reversed = UTF8::strrev($this->str);
1679
1680 5
    return static::create($reversed, $this->encoding);
1681
  }
1682
1683
  /**
1684
   * Truncates the string to a given length, while ensuring that it does not
1685
   * split words. If $substring is provided, and truncating occurs, the
1686
   * string is further truncated so that the substring may be appended without
1687
   * exceeding the desired length.
1688
   *
1689
   * @param int    $length    <p>Desired length of the truncated string.</p>
1690
   * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
1691
   *
1692
   * @return static <p>Object with the resulting $str after truncating.</p>
1693
   */
1694 23
  public function safeTruncate(int $length, string $substring = ''): self
1695
  {
1696 23
    $stringy = static::create($this->str, $this->encoding);
1697 23
    if ($length >= $stringy->length()) {
1698 4
      return $stringy;
1699
    }
1700
1701
    // need to further trim the string so we can append the substring
1702 19
    $encoding = $stringy->encoding;
1703 19
    $substringLength = UTF8::strlen($substring, $encoding);
1704 19
    $length -= $substringLength;
1705
1706 19
    $truncated = UTF8::substr($stringy->str, 0, $length, $encoding);
1707
1708
    // if the last word was truncated
1709 19
    $strPosSpace = UTF8::strpos($stringy->str, ' ', $length - 1, $encoding);
1710 19
    if ($strPosSpace != $length) {
1711
      // find pos of the last occurrence of a space, get up to that
1712 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 1706 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...
1713
1714 12
      if ($lastPos !== false || $strPosSpace !== false) {
1715 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 1715 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...
1716
      }
1717
    }
1718
1719 19
    $stringy->str = $truncated . $substring;
1720
1721 19
    return $stringy;
1722
  }
1723
1724
  /**
1725
   * A multibyte string shuffle function. It returns a string with its
1726
   * characters in random order.
1727
   *
1728
   * @return static <p>Object with a shuffled $str.</p>
1729
   */
1730 3
  public function shuffle(): self
1731
  {
1732 3
    $shuffledStr = UTF8::str_shuffle($this->str);
1733
1734 3
    return static::create($shuffledStr, $this->encoding);
1735
  }
1736
1737
  /**
1738
   * Converts the string into an URL slug. This includes replacing non-ASCII
1739
   * characters with their closest ASCII equivalents, removing remaining
1740
   * non-ASCII and non-alphanumeric characters, and replacing whitespace with
1741
   * $replacement. The replacement defaults to a single dash, and the string
1742
   * is also converted to lowercase.
1743
   *
1744
   * @param string $replacement [optional] <p>The string used to replace whitespace. Default: '-'</p>
1745
   * @param string $language    [optional] <p>The language for the url. Default: 'de'</p>
1746
   * @param bool   $strToLower  [optional] <p>string to lower. Default: true</p>
1747
   *
1748
   * @return static <p>Object whose $str has been converted to an URL slug.</p>
1749
   */
1750 15
  public function slugify(string $replacement = '-', string $language = 'de', bool $strToLower = true): self
1751
  {
1752 15
    $slug = URLify::slug($this->str, $language, $replacement, $strToLower);
1753
1754 15
    return static::create($slug, $this->encoding);
1755
  }
1756
1757
  /**
1758
   * Remove css media-queries.
1759
   *
1760
   * @return static
1761
   */
1762 1
  public function stripeCssMediaQueries(): self
1763
  {
1764 1
    $pattern = '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU';
1765
1766 1
    return static::create(\preg_replace($pattern, '', $this->str));
1767
  }
1768
1769
  /**
1770
   * Strip all whitespace characters. This includes tabs and newline characters,
1771
   * as well as multibyte whitespace such as the thin space and ideographic space.
1772
   *
1773
   * @return static
1774
   */
1775 12
  public function stripWhitespace(): self
1776
  {
1777 12
    return static::create(UTF8::strip_whitespace($this->str));
1778
  }
1779
1780
  /**
1781
   * Remove empty html-tag.
1782
   *
1783
   * e.g.: <tag></tag>
1784
   *
1785
   * @return static
1786
   */
1787 1
  public function stripeEmptyHtmlTags(): self
1788
  {
1789 1
    $pattern = "/<[^\/>]*>(([\s]?)*|)<\/[^>]*>/i";
1790
1791 1
    return static::create(\preg_replace($pattern, '', $this->str));
1792
  }
1793
1794
  /**
1795
   * Converts the string into an valid UTF-8 string.
1796
   *
1797
   * @return static
1798
   */
1799 1
  public function utf8ify(): self
1800
  {
1801 1
    return static::create(UTF8::cleanup($this->str));
1802
  }
1803
1804
  /**
1805
   * Create a escape html version of the string via "UTF8::htmlspecialchars()".
1806
   *
1807
   * @return static
1808
   */
1809 6
  public function escape(): self
1810
  {
1811 6
    $str = UTF8::htmlspecialchars(
1812 6
        $this->str,
1813 6
        ENT_QUOTES | ENT_SUBSTITUTE,
1814 6
        $this->encoding
1815
    );
1816
1817 6
    return static::create($str, $this->encoding);
1818
  }
1819
1820
  /**
1821
   * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1822
   *
1823
   * @param string   $search
1824
   * @param int|null $length                 [optional] <p>Default: null === text->length / 2</p>
1825
   * @param string   $replacerForSkippedText [optional] <p>Default: …</p>
1826
   *
1827
   * @return static
1828
   */
1829 1
  public function extractText(string $search = '', int $length = null, string $replacerForSkippedText = '…'): self
1830
  {
1831
    // init
1832 1
    $text = $this->str;
1833
1834 1
    if (empty($text)) {
1835 1
      return static::create('', $this->encoding);
1836
    }
1837
1838 1
    $trimChars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1839
1840 1
    if ($length === null) {
1841 1
      $length = (int)\round($this->length() / 2, 0);
1842
    }
1843
1844 1
    if (empty($search)) {
1845
1846 1
      $stringLength = UTF8::strlen($text, $this->encoding);
1847
1848 1
      if ($length > 0) {
1849 1
        $end = ($length - 1) > $stringLength ? $stringLength : ($length - 1);
1850
      } else {
1851 1
        $end = 0;
1852
      }
1853
1854 1
      $pos = \min(
1855 1
          UTF8::strpos($text, ' ', $end, $this->encoding),
1856 1
          UTF8::strpos($text, '.', $end, $this->encoding)
1857
      );
1858
1859 1
      if ($pos) {
1860 1
        return static::create(
1861 1
            \rtrim(
1862 1
                UTF8::substr($text, 0, $pos, $this->encoding),
1863 1
                $trimChars
1864 1
            ) . $replacerForSkippedText,
1865 1
            $this->encoding
1866
        );
1867
      }
1868
1869
      return static::create($text, $this->encoding);
1870
    }
1871
1872 1
    $wordPos = UTF8::stripos(
1873 1
        $text,
1874 1
        $search,
1875 1
        0,
1876 1
        $this->encoding
1877
    );
1878 1
    $halfSide = (int)($wordPos - $length / 2 + UTF8::strlen($search, $this->encoding) / 2);
1879
1880 1
    if ($halfSide > 0) {
1881
1882 1
      $halfText = UTF8::substr($text, 0, $halfSide, $this->encoding);
1883 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 1882 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...
1884
1885 1
      if (!$pos_start) {
1886 1
        $pos_start = 0;
1887
      }
1888
1889
    } else {
1890 1
      $pos_start = 0;
1891
    }
1892
1893 1
    if ($wordPos && $halfSide > 0) {
1894 1
      $l = $pos_start + $length - 1;
1895 1
      $realLength = UTF8::strlen($text, $this->encoding);
1896
1897 1
      if ($l > $realLength) {
1898
        $l = $realLength;
1899
      }
1900
1901 1
      $pos_end = \min(
1902 1
                     UTF8::strpos($text, ' ', $l, $this->encoding),
1903 1
                     UTF8::strpos($text, '.', $l, $this->encoding)
1904 1
                 ) - $pos_start;
1905
1906 1
      if (!$pos_end || $pos_end <= 0) {
1907 1
        $extract = $replacerForSkippedText . \ltrim(
1908 1
                UTF8::substr(
1909 1
                    $text,
1910 1
                    $pos_start,
1911 1
                    UTF8::strlen($text),
1912 1
                    $this->encoding
1913
                ),
1914 1
                $trimChars
1915
            );
1916 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...
1917 1
        $extract = $replacerForSkippedText . \trim(
1918 1
                UTF8::substr(
1919 1
                    $text,
1920 1
                    $pos_start,
1921 1
                    $pos_end,
1922 1
                    $this->encoding
1923
                ),
1924 1
                $trimChars
1925 1
            ) . $replacerForSkippedText;
1926
      }
1927
1928
    } else {
1929
1930 1
      $l = $length - 1;
1931 1
      $trueLength = UTF8::strlen($text, $this->encoding);
1932
1933 1
      if ($l > $trueLength) {
1934
        $l = $trueLength;
1935
      }
1936
1937 1
      $pos_end = \min(
1938 1
          UTF8::strpos($text, ' ', $l, $this->encoding),
1939 1
          UTF8::strpos($text, '.', $l, $this->encoding)
1940
      );
1941
1942 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...
1943 1
        $extract = \rtrim(
1944 1
                       UTF8::substr($text, 0, $pos_end, $this->encoding),
1945 1
                       $trimChars
1946 1
                   ) . $replacerForSkippedText;
1947
      } else {
1948 1
        $extract = $text;
1949
      }
1950
    }
1951
1952 1
    return static::create($extract, $this->encoding);
1953
  }
1954
1955
1956
  /**
1957
   * Try to remove all XSS-attacks from the string.
1958
   *
1959
   * @return static
1960
   */
1961 6
  public function removeXss(): self
1962
  {
1963 6
    static $antiXss = null;
1964
1965 6
    if ($antiXss === null) {
1966 1
      $antiXss = new AntiXSS();
1967
    }
1968
1969 6
    $str = $antiXss->xss_clean($this->str);
1970
1971 6
    return static::create($str, $this->encoding);
1972
  }
1973
1974
  /**
1975
   * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
1976
   *
1977
   * @param string $replacement [optional] <p>Default is a empty string.</p>
1978
   *
1979
   * @return static
1980
   */
1981 6
  public function removeHtmlBreak(string $replacement = ''): self
1982
  {
1983 6
    $str = (string)\preg_replace('#/\r\n|\r|\n|<br.*/?>#isU', $replacement, $this->str);
1984
1985 6
    return static::create($str, $this->encoding);
1986
  }
1987
1988
  /**
1989
   * Remove html via "strip_tags()" from the string.
1990
   *
1991
   * @param string $allowableTags [optional] <p>You can use the optional second parameter to specify tags which should
1992
   *                              not be stripped. Default: null
1993
   *                              </p>
1994
   *
1995
   * @return static
1996
   */
1997 6
  public function removeHtml(string $allowableTags = null): self
1998
  {
1999 6
    $str = \strip_tags($this->str, $allowableTags);
2000
2001 6
    return static::create($str, $this->encoding);
2002
  }
2003
2004
  /**
2005
   * Returns the substring beginning at $start, and up to, but not including
2006
   * the index specified by $end. If $end is omitted, the function extracts
2007
   * the remaining string. If $end is negative, it is computed from the end
2008
   * of the string.
2009
   *
2010
   * @param int $start <p>Initial index from which to begin extraction.</p>
2011
   * @param int $end   [optional] <p>Index at which to end extraction. Default: null</p>
2012
   *
2013
   * @return static <p>Object with its $str being the extracted substring.</p>
2014
   */
2015 18
  public function slice(int $start, int $end = null): self
2016
  {
2017 18
    if ($end === null) {
2018 4
      $length = $this->length();
2019 14
    } elseif ($end >= 0 && $end <= $start) {
2020 4
      return static::create('', $this->encoding);
2021 10
    } elseif ($end < 0) {
2022 2
      $length = $this->length() + $end - $start;
2023
    } else {
2024 8
      $length = $end - $start;
2025
    }
2026
2027 14
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
2028
2029 14
    return static::create($str, $this->encoding);
2030
  }
2031
2032
  /**
2033
   * Splits the string with the provided regular expression, returning an
2034
   * array of Stringy objects. An optional integer $limit will truncate the
2035
   * results.
2036
   *
2037
   * @param string $pattern <p>The regex with which to split the string.</p>
2038
   * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
2039
   *
2040
   * @return static[] <p>An array of Stringy objects.</p>
2041
   */
2042 16
  public function split(string $pattern, int $limit = -1): array
2043
  {
2044 16
    if ($limit === 0) {
2045 2
      return [];
2046
    }
2047
2048
    // this->split errors when supplied an empty pattern in < PHP 5.4.13
2049
    // and current versions of HHVM (3.8 and below)
2050 14
    if ($pattern === '') {
2051 1
      return [static::create($this->str, $this->encoding)];
2052
    }
2053
2054
    // this->split returns the remaining unsplit string in the last index when
2055
    // supplying a limit
2056 13
    if ($limit > 0) {
2057 8
      $limit += 1;
2058
    } else {
2059 5
      $limit = -1;
2060
    }
2061
2062 13
    $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $this->str, $limit);
2063
2064 13
    if ($limit > 0 && \count($array) === $limit) {
2065 4
      \array_pop($array);
2066
    }
2067
2068
    /** @noinspection CallableInLoopTerminationConditionInspection */
2069
    /** @noinspection ForeachInvariantsInspection */
2070 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...
2071 13
      $array[$i] = static::create($array[$i], $this->encoding);
2072
    }
2073
2074 13
    return $array;
2075
  }
2076
2077
  /**
2078
   * Surrounds $str with the given substring.
2079
   *
2080
   * @param string $substring <p>The substring to add to both sides.</P>
2081
   *
2082
   * @return static <p>Object whose $str had the substring both prepended and appended.</p>
2083
   */
2084 5
  public function surround(string $substring): self
2085
  {
2086 5
    $str = \implode('', [$substring, $this->str, $substring]);
2087
2088 5
    return static::create($str, $this->encoding);
2089
  }
2090
2091
  /**
2092
   * Returns a case swapped version of the string.
2093
   *
2094
   * @return static <p>Object whose $str has each character's case swapped.</P>
2095
   */
2096 5
  public function swapCase(): self
2097
  {
2098 5
    $stringy = static::create($this->str, $this->encoding);
2099
2100 5
    $stringy->str = UTF8::swapCase($stringy->str, $stringy->encoding);
2101
2102 5
    return $stringy;
2103
  }
2104
2105
  /**
2106
   * Returns a string with smart quotes, ellipsis characters, and dashes from
2107
   * Windows-1252 (commonly used in Word documents) replaced by their ASCII
2108
   * equivalents.
2109
   *
2110
   * @return static <p>Object whose $str has those characters removed.</p>
2111
   */
2112 4
  public function tidy(): self
2113
  {
2114 4
    $str = UTF8::normalize_msword($this->str);
2115
2116 4
    return static::create($str, $this->encoding);
2117
  }
2118
2119
  /**
2120
   * Returns a trimmed string in proper title case.
2121
   *
2122
   * Also accepts an array, $ignore, allowing you to list words not to be
2123
   * capitalized.
2124
   *
2125
   * Adapted from John Gruber's script.
2126
   *
2127
   * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
2128
   *
2129
   * @param array $ignore <p>An array of words not to capitalize.</p>
2130
   *
2131
   * @return static <p>Object with a titleized $str</p>
2132
   */
2133 35
  public function titleizeForHumans(array $ignore = []): self
2134
  {
2135 35
    $smallWords = \array_merge(
2136
        [
2137 35
            '(?<!q&)a',
2138
            'an',
2139
            'and',
2140
            'as',
2141
            'at(?!&t)',
2142
            'but',
2143
            'by',
2144
            'en',
2145
            'for',
2146
            'if',
2147
            'in',
2148
            'of',
2149
            'on',
2150
            'or',
2151
            'the',
2152
            'to',
2153
            'v[.]?',
2154
            'via',
2155
            'vs[.]?',
2156
        ],
2157 35
        $ignore
2158
    );
2159
2160 35
    $smallWordsRx = \implode('|', $smallWords);
2161 35
    $apostropheRx = '(?x: [\'’] [[:lower:]]* )?';
2162 35
    $stringy = static::create($this->trim(), $this->encoding);
2163
2164 35
    if (\preg_match('/[[:lower:]]/', (string)$stringy) === 0) {
2165 3
      $stringy = $stringy->toLowerCase();
2166
    }
2167
2168
    // The main substitutions
2169 35
    $stringy->str = \preg_replace_callback(
2170
        '~\b (_*) (?:                                                              # 1. Leading underscore and
2171
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |              # 2. file path or 
2172 35
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostropheRx . ' ) #    URL, domain, or email
2173
                        |
2174 35
                        ( (?i: ' . $smallWordsRx . ' ) ' . $apostropheRx . ' )            # 3. or small word (case-insensitive)
2175
                        |
2176 35
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostropheRx . ' )     # 4. or word w/o internal caps
2177
                        |
2178 35
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostropheRx . ' )     # 5. or some other word
2179
                      ) (_*) \b                                                           # 6. With trailing underscore
2180
                    ~ux',
2181 35
        function ($matches) {
2182
          // Preserve leading underscore
2183 35
          $str = $matches[1];
2184 35
          if ($matches[2]) {
2185
            // Preserve URLs, domains, emails and file paths
2186 5
            $str .= $matches[2];
2187 35
          } elseif ($matches[3]) {
2188
            // Lower-case small words
2189 25
            $str .= static::create($matches[3], $this->encoding)->toLowerCase();
2190 35
          } elseif ($matches[4]) {
2191
            // Capitalize word w/o internal caps
2192 34
            $str .= static::create($matches[4], $this->encoding)->upperCaseFirst();
2193
          } else {
2194
            // Preserve other kinds of word (iPhone)
2195 7
            $str .= $matches[5];
2196
          }
2197
          // Preserve trailing underscore
2198 35
          $str .= $matches[6];
2199
2200 35
          return $str;
2201 35
        },
2202 35
        $stringy->str
2203
    );
2204
2205
    // Exceptions for small words: capitalize at start of title...
2206 35
    $stringy->str = \preg_replace_callback(
2207
        '~(  \A [[:punct:]]*                # start of title...
2208
                      |  [:.;?!][ ]+               # or of subsentence...
2209
                      |  [ ][\'"“‘(\[][ ]* )       # or of inserted subphrase...
2210 35
                      ( ' . $smallWordsRx . ' ) \b # ...followed by small word
2211
                     ~uxi',
2212 35
        function ($matches) {
2213 11
          return $matches[1] . static::create($matches[2], $this->encoding)->upperCaseFirst();
2214 35
        },
2215 35
        $stringy->str
2216
    );
2217
2218
    // ...and end of title
2219 35
    $stringy->str = \preg_replace_callback(
2220 35
        '~\b ( ' . $smallWordsRx . ' ) # small word...
2221
                      (?= [[:punct:]]* \Z     # ...at the end of the title...
2222
                      |   [\'"’”)\]] [ ] )    # ...or of an inserted subphrase?
2223
                     ~uxi',
2224 35
        function ($matches) {
2225 3
          return static::create($matches[1], $this->encoding)->upperCaseFirst();
2226 35
        },
2227 35
        $stringy->str
2228
    );
2229
2230
    // Exceptions for small words in hyphenated compound words
2231
    // e.g. "in-flight" -> In-Flight
2232 35
    $stringy->str = \preg_replace_callback(
2233
        '~\b
2234
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
2235 35
                        ( ' . $smallWordsRx . ' )
2236
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
2237
                       ~uxi',
2238 35
        function ($matches) {
2239
          return static::create($matches[1], $this->encoding)->upperCaseFirst();
2240 35
        },
2241 35
        $stringy->str
2242
    );
2243
2244
    // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
2245 35
    $stringy->str = \preg_replace_callback(
2246
        '~\b
2247
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
2248
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
2249 35
                      ( ' . $smallWordsRx . ' ) # ...followed by small word
2250
                      (?!	- )                   # Negative lookahead for another -
2251
                     ~uxi',
2252 35
        function ($matches) {
2253
          return $matches[1] . static::create($matches[2], $this->encoding)->upperCaseFirst();
2254 35
        },
2255 35
        $stringy->str
2256
    );
2257
2258 35
    return $stringy;
2259
  }
2260
2261
  /**
2262
   * Returns a trimmed string with the first letter of each word capitalized.
2263
   * Also accepts an array, $ignore, allowing you to list words not to be
2264
   * capitalized.
2265
   *
2266
   * @param array|null $ignore [optional] <p>An array of words not to capitalize or null. Default: null</p>
2267
   *
2268
   * @return static <p>Object with a titleized $str.</p>
2269
   */
2270 5
  public function titleize(array $ignore = null): self
2271
  {
2272 5
    $stringy = static::create($this->trim(), $this->encoding);
2273 5
    $encoding = $this->encoding;
2274
2275 5
    $stringy->str = (string)\preg_replace_callback(
2276 5
        '/([\S]+)/u',
2277 5
        function ($match) use ($encoding, $ignore) {
2278 5
          if ($ignore && \in_array($match[0], $ignore, true)) {
2279 2
            return $match[0];
2280
          }
2281
2282 5
          $stringy = new static($match[0], $encoding);
2283
2284 5
          return (string)$stringy->toLowerCase()->upperCaseFirst();
2285 5
        },
2286 5
        $stringy->str
2287
    );
2288
2289 5
    return $stringy;
2290
  }
2291
2292
  /**
2293
   * Converts all characters in the string to lowercase.
2294
   *
2295
   * @return static <p>Object with all characters of $str being lowercase.</p>
2296
   */
2297 54
  public function toLowerCase(): self
2298
  {
2299 54
    $str = UTF8::strtolower($this->str, $this->encoding);
2300
2301 54
    return static::create($str, $this->encoding);
2302
  }
2303
2304
  /**
2305
   * Returns true if the string is base64 encoded, false otherwise.
2306
   *
2307
   * @return bool <p>Whether or not $str is base64 encoded.</p>
2308
   */
2309 7
  public function isBase64(): bool
2310
  {
2311 7
    return UTF8::is_base64($this->str);
2312
  }
2313
2314
  /**
2315
   * Returns an ASCII version of the string. A set of non-ASCII characters are
2316
   * replaced with their closest ASCII counterparts, and the rest are removed
2317
   * unless instructed otherwise.
2318
   *
2319
   * @param bool $strict [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad performance |
2320
   *                     Default: false</p>
2321
   *
2322
   * @return static <p>Object whose $str contains only ASCII characters.</p>
2323
   */
2324 16
  public function toAscii(bool $strict = false): self
2325
  {
2326 16
    $str = UTF8::to_ascii($this->str, '?', $strict);
2327
2328 16
    return static::create($str, $this->encoding);
2329
  }
2330
2331
  /**
2332
   * Returns a boolean representation of the given logical string value.
2333
   * For example, 'true', '1', 'on' and 'yes' will return true. 'false', '0',
2334
   * 'off', and 'no' will return false. In all instances, case is ignored.
2335
   * For other numeric strings, their sign will determine the return value.
2336
   * In addition, blank strings consisting of only whitespace will return
2337
   * false. For all other strings, the return value is a result of a
2338
   * boolean cast.
2339
   *
2340
   * @return bool <p>A boolean value for the string.</p>
2341
   */
2342 15
  public function toBoolean(): bool
2343
  {
2344 15
    $key = $this->toLowerCase()->str;
2345
    $map = [
2346 15
        'true'  => true,
2347
        '1'     => true,
2348
        'on'    => true,
2349
        'yes'   => true,
2350
        'false' => false,
2351
        '0'     => false,
2352
        'off'   => false,
2353
        'no'    => false,
2354
    ];
2355
2356 15
    if (\array_key_exists($key, $map)) {
2357 10
      return $map[$key];
2358
    }
2359
2360 5
    if (\is_numeric($this->str)) {
2361 2
      return ((int)$this->str > 0);
2362
    }
2363
2364 3
    return (bool)$this->regexReplace('[[:space:]]', '')->str;
2365
  }
2366
2367
  /**
2368
   * Return Stringy object as string, but you can also use (string) for automatically casting the object into a string.
2369
   *
2370
   * @return string
2371
   */
2372 1076
  public function toString(): string
2373
  {
2374 1076
    return (string)$this->str;
2375
  }
2376
2377
  /**
2378
   * Converts each tab in the string to some number of spaces, as defined by
2379
   * $tabLength. By default, each tab is converted to 4 consecutive spaces.
2380
   *
2381
   * @param int $tabLength [optional] <p>Number of spaces to replace each tab with. Default: 4</p>
2382
   *
2383
   * @return static <p>Object whose $str has had tabs switched to spaces.</p>
2384
   */
2385 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...
2386
  {
2387 6
    $spaces = UTF8::str_repeat(' ', $tabLength);
2388 6
    $str = UTF8::str_replace("\t", $spaces, $this->str);
2389
2390 6
    return static::create($str, $this->encoding);
2391
  }
2392
2393
  /**
2394
   * Converts each occurrence of some consecutive number of spaces, as
2395
   * defined by $tabLength, to a tab. By default, each 4 consecutive spaces
2396
   * are converted to a tab.
2397
   *
2398
   * @param int $tabLength [optional] <p>Number of spaces to replace with a tab. Default: 4</p>
2399
   *
2400
   * @return static <p>Object whose $str has had spaces switched to tabs.</p>
2401
   */
2402 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...
2403
  {
2404 5
    $spaces = UTF8::str_repeat(' ', $tabLength);
2405 5
    $str = UTF8::str_replace($spaces, "\t", $this->str);
2406
2407 5
    return static::create($str, $this->encoding);
2408
  }
2409
2410
  /**
2411
   * Converts the first character of each word in the string to uppercase.
2412
   *
2413
   * @return static  Object with all characters of $str being title-cased
2414
   */
2415 5
  public function toTitleCase(): self
2416
  {
2417
    // "mb_convert_case()" used a polyfill from the "UTF8"-Class
2418 5
    $str = \mb_convert_case($this->str, MB_CASE_TITLE, $this->encoding);
2419
2420 5
    return static::create($str, $this->encoding);
2421
  }
2422
2423
  /**
2424
   * Converts all characters in the string to uppercase.
2425
   *
2426
   * @return static  Object with all characters of $str being uppercase
2427
   */
2428 5
  public function toUpperCase(): self
2429
  {
2430 5
    $str = UTF8::strtoupper($this->str, $this->encoding);
2431
2432 5
    return static::create($str, $this->encoding);
2433
  }
2434
2435
  /**
2436
   * Returns a string with whitespace removed from the start of the string.
2437
   * Supports the removal of unicode whitespace. Accepts an optional
2438
   * string of characters to strip instead of the defaults.
2439
   *
2440
   * @param string $chars [optional] <p>Optional string of characters to strip. Default: null</p>
2441
   *
2442
   * @return static <p>Object with a trimmed $str.</p>
2443
   */
2444 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...
2445
  {
2446 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...
2447 11
      $chars = '[:space:]';
2448
    } else {
2449 2
      $chars = \preg_quote($chars, '/');
2450
    }
2451
2452 13
    return $this->regexReplace("^[$chars]+", '');
2453
  }
2454
2455
  /**
2456
   * Returns a string with whitespace removed from the end of the string.
2457
   * Supports the removal of unicode whitespace. Accepts an optional
2458
   * string of characters to strip instead of the defaults.
2459
   *
2460
   * @param string $chars [optional] <p>Optional string of characters to strip. Default: null</p>
2461
   *
2462
   * @return static <p>Object with a trimmed $str.</p>
2463
   */
2464 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...
2465
  {
2466 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...
2467 11
      $chars = '[:space:]';
2468
    } else {
2469 2
      $chars = \preg_quote($chars, '/');
2470
    }
2471
2472 13
    return $this->regexReplace("[$chars]+\$", '');
2473
  }
2474
2475
  /**
2476
   * Truncates the string to a given length. If $substring is provided, and
2477
   * truncating occurs, the string is further truncated so that the substring
2478
   * may be appended without exceeding the desired length.
2479
   *
2480
   * @param int    $length    <p>Desired length of the truncated string.</p>
2481
   * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
2482
   *
2483
   * @return static <p>Object with the resulting $str after truncating.</p>
2484
   */
2485 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...
2486
  {
2487 22
    $stringy = static::create($this->str, $this->encoding);
2488 22
    if ($length >= $stringy->length()) {
2489 4
      return $stringy;
2490
    }
2491
2492
    // Need to further trim the string so we can append the substring
2493 18
    $substringLength = UTF8::strlen($substring, $stringy->encoding);
2494 18
    $length -= $substringLength;
2495
2496 18
    $truncated = UTF8::substr($stringy->str, 0, $length, $stringy->encoding);
2497 18
    $stringy->str = $truncated . $substring;
2498
2499 18
    return $stringy;
2500
  }
2501
2502
  /**
2503
   * Returns a lowercase and trimmed string separated by underscores.
2504
   * Underscores are inserted before uppercase characters (with the exception
2505
   * of the first character of the string), and in place of spaces as well as
2506
   * dashes.
2507
   *
2508
   * @return static <p>Object with an underscored $str.</p>
2509
   */
2510 16
  public function underscored(): self
2511
  {
2512 16
    return $this->delimit('_');
2513
  }
2514
2515
  /**
2516
   * Returns an UpperCamelCase version of the supplied string. It trims
2517
   * surrounding spaces, capitalizes letters following digits, spaces, dashes
2518
   * and underscores, and removes spaces, dashes, underscores.
2519
   *
2520
   * @return static  <p>Object with $str in UpperCamelCase.</p>
2521
   */
2522 13
  public function upperCamelize(): self
2523
  {
2524 13
    return $this->camelize()->upperCaseFirst();
2525
  }
2526
2527
  /**
2528
   * Returns a camelCase version of the string. Trims surrounding spaces,
2529
   * capitalizes letters following digits, spaces, dashes and underscores,
2530
   * and removes spaces, dashes, as well as underscores.
2531
   *
2532
   * @return static <p>Object with $str in camelCase.</p>
2533
   */
2534 32
  public function camelize(): self
2535
  {
2536 32
    $encoding = $this->encoding;
2537 32
    $stringy = $this->trim()->lowerCaseFirst();
2538 32
    $stringy->str = (string)\preg_replace('/^[-_]+/', '', $stringy->str);
2539
2540 32
    $stringy->str = (string)\preg_replace_callback(
2541 32
        '/[-_\s]+(.)?/u',
2542 32
        function ($match) use ($encoding) {
2543 27
          if (isset($match[1])) {
2544 27
            return UTF8::strtoupper($match[1], $encoding);
2545
          }
2546
2547 1
          return '';
2548 32
        },
2549 32
        $stringy->str
2550
    );
2551
2552 32
    $stringy->str = (string)\preg_replace_callback(
2553 32
        '/[\d]+(.)?/u',
2554 32
        function ($match) use ($encoding) {
2555 6
          return UTF8::strtoupper($match[0], $encoding);
2556 32
        },
2557 32
        $stringy->str
2558
    );
2559
2560 32
    return $stringy;
2561
  }
2562
2563
  /**
2564
   * Convert a string to e.g.: "snake_case"
2565
   *
2566
   * @return static <p>Object with $str in snake_case.</p>
2567
   */
2568 20
  public function snakeize(): self
2569
  {
2570 20
    $str = $this->str;
2571
2572 20
    $encoding = $this->encoding;
2573 20
    $str = UTF8::normalize_whitespace($str);
2574 20
    $str = \str_replace('-', '_', $str);
2575
2576 20
    $str = (string)\preg_replace_callback(
2577 20
        '/([\d|A-Z])/u',
2578 20
        function ($matches) use ($encoding) {
2579 8
          $match = $matches[1];
2580 8
          $matchInt = (int)$match;
2581
2582 8
          if ("$matchInt" == $match) {
2583 4
            return '_' . $match . '_';
2584
          }
2585
2586 4
          return '_' . UTF8::strtolower($match, $encoding);
2587 20
        },
2588 20
        $str
2589
    );
2590
2591 20
    $str = (string)\preg_replace(
2592
        [
2593
2594 20
            '/\s+/',      // convert spaces to "_"
2595
            '/^\s+|\s+$/',  // trim leading & trailing spaces
2596
            '/_+/',         // remove double "_"
2597
        ],
2598
        [
2599 20
            '_',
2600
            '',
2601
            '_',
2602
        ],
2603 20
        $str
2604
    );
2605
2606 20
    $str = UTF8::trim($str, '_'); // trim leading & trailing "_"
2607 20
    $str = UTF8::trim($str); // trim leading & trailing whitespace
2608
2609 20
    return static::create($str, $this->encoding);
2610
  }
2611
2612
  /**
2613
   * Converts the first character of the string to lower case.
2614
   *
2615
   * @return static <p>Object with the first character of $str being lower case.</p>
2616
   */
2617 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...
2618
  {
2619 37
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
2620 37
    $rest = UTF8::substr($this->str, 1, $this->length() - 1, $this->encoding);
2621
2622 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 2619 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...
2623
2624 37
    return static::create($str, $this->encoding);
2625
  }
2626
2627
  /**
2628
   * Shorten the string after $length, but also after the next word.
2629
   *
2630
   * @param int    $length
2631
   * @param string $strAddOn [optional] <p>Default: '…'</p>
2632
   *
2633
   * @return static
2634
   */
2635 4
  public function shortenAfterWord(int $length, string $strAddOn = '…'): self
2636
  {
2637 4
    $string = UTF8::str_limit_after_word($this->str, $length, $strAddOn);
2638
2639 4
    return static::create($string);
2640
  }
2641
2642
  /**
2643
   * Line-Wrap the string after $limit, but also after the next word.
2644
   *
2645
   * @param int $limit
2646
   *
2647
   * @return static
2648
   */
2649 1
  public function lineWrapAfterWord(int $limit): self
2650
  {
2651 1
    $strings = (array)\preg_split('/\\r\\n|\\r|\\n/', $this->str);
2652
2653 1
    $string = '';
2654 1
    foreach ($strings as $value) {
2655 1
      $string .= wordwrap($value, $limit);
2656 1
      $string .= "\n";
2657
    }
2658
2659 1
    return static::create($string);
2660
  }
2661
2662
  /**
2663
   * Gets the substring after the first occurrence of a separator.
2664
   * If no match is found returns new empty Stringy object.
2665
   *
2666
   * @param string $separator
2667
   *
2668
   * @return static
2669
   */
2670 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...
2671
  {
2672 2
    if ($separator === '') {
2673
      return static::create();
2674
    }
2675
2676 2
    if ($this->str === '') {
2677 1
      return static::create();
2678
    }
2679
2680 2
    if (($offset = $this->indexOf($separator)) === false) {
2681 2
      return static::create();
2682
    }
2683
2684 2
    return static::create(
2685 2
        UTF8::substr(
2686 2
            $this->str,
2687 2
            $offset + UTF8::strlen($separator, $this->encoding),
2688 2
            null,
2689 2
            $this->encoding
2690
        ),
2691 2
        $this->encoding
2692
    );
2693
  }
2694
2695
  /**
2696
   * Gets the substring after the first occurrence of a separator.
2697
   * If no match is found returns new empty Stringy object.
2698
   *
2699
   * @param string $separator
2700
   *
2701
   * @return static
2702
   */
2703 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...
2704
  {
2705 1
    if ($separator === '') {
2706
      return static::create();
2707
    }
2708
2709 1
    if ($this->str === '') {
2710 1
      return static::create();
2711
    }
2712
2713 1
    if (($offset = $this->indexOfIgnoreCase($separator)) === false) {
2714 1
      return static::create();
2715
    }
2716
2717 1
    return static::create(
2718 1
        UTF8::substr(
2719 1
            $this->str,
2720 1
            $offset + UTF8::strlen($separator, $this->encoding),
2721 1
            null,
2722 1
            $this->encoding
2723
        ),
2724 1
        $this->encoding
2725
    );
2726
  }
2727
2728
  /**
2729
   * Gets the substring after the last occurrence of a separator.
2730
   * If no match is found returns new empty Stringy object.
2731
   *
2732
   * @param string $separator
2733
   *
2734
   * @return static
2735
   */
2736 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...
2737
  {
2738 1
    if ($separator === '') {
2739
      return static::create();
2740
    }
2741
2742 1
    if ($this->str === '') {
2743 1
      return static::create();
2744
    }
2745
2746 1
    $offset = $this->indexOfLastIgnoreCase($separator);
2747 1
    if ($offset === false) {
2748 1
      return static::create('', $this->encoding);
2749
    }
2750
2751 1
    return static::create(
2752 1
        UTF8::substr(
2753 1
            $this->str,
2754 1
            $offset + UTF8::strlen($separator, $this->encoding),
2755 1
            null,
2756 1
            $this->encoding
2757
        ),
2758 1
        $this->encoding
2759
    );
2760
  }
2761
2762
  /**
2763
   * Gets the substring after the last occurrence of a separator.
2764
   * If no match is found returns new empty Stringy object.
2765
   *
2766
   * @param string $separator
2767
   *
2768
   * @return static
2769
   */
2770 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...
2771
  {
2772 1
    if ($separator === '') {
2773
      return static::create();
2774
    }
2775
2776 1
    if ($this->str === '') {
2777 1
      return static::create();
2778
    }
2779
2780 1
    $offset = $this->indexOfLast($separator);
2781 1
    if ($offset === false) {
2782 1
      return static::create('', $this->encoding);
2783
    }
2784
2785 1
    return static::create(
2786 1
        UTF8::substr(
2787 1
            $this->str,
2788 1
            $offset + UTF8::strlen($separator, $this->encoding),
2789 1
            null,
2790 1
            $this->encoding
2791
        ),
2792 1
        $this->encoding
2793
    );
2794
  }
2795
2796
  /**
2797
   * Gets the substring before the first occurrence of a separator.
2798
   * If no match is found returns new empty Stringy object.
2799
   *
2800
   * @param string $separator
2801
   *
2802
   * @return static
2803
   */
2804 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...
2805
  {
2806 1
    if ($separator === '') {
2807
      return static::create();
2808
    }
2809
2810 1
    if ($this->str === '') {
2811 1
      return static::create();
2812
    }
2813
2814 1
    $offset = $this->indexOf($separator);
2815 1
    if ($offset === false) {
2816 1
      return static::create('', $this->encoding);
2817
    }
2818
2819 1
    return static::create(
2820 1
        UTF8::substr(
2821 1
            $this->str,
2822 1
            0,
2823 1
            $offset,
2824 1
            $this->encoding
2825
        ),
2826 1
        $this->encoding
2827
    );
2828
  }
2829
2830
  /**
2831
   * Gets the substring before the first occurrence of a separator.
2832
   * If no match is found returns new empty Stringy object.
2833
   *
2834
   * @param string $separator
2835
   *
2836
   * @return static
2837
   */
2838 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...
2839
  {
2840 1
    if ($separator === '') {
2841
      return static::create();
2842
    }
2843
2844 1
    if ($this->str === '') {
2845 1
      return static::create();
2846
    }
2847
2848 1
    $offset = $this->indexOfIgnoreCase($separator);
2849 1
    if ($offset === false) {
2850 1
      return static::create('', $this->encoding);
2851
    }
2852
2853 1
    return static::create(
2854 1
        UTF8::substr(
2855 1
            $this->str,
2856 1
            0,
2857 1
            $offset,
2858 1
            $this->encoding
2859
        ),
2860 1
        $this->encoding
2861
    );
2862
  }
2863
2864
  /**
2865
   * Gets the substring before the last occurrence of a separator.
2866
   * If no match is found returns new empty Stringy object.
2867
   *
2868
   * @param string $separator
2869
   *
2870
   * @return static
2871
   */
2872 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...
2873
  {
2874 1
    if ($separator === '') {
2875
      return static::create();
2876
    }
2877
2878 1
    if ($this->str === '') {
2879 1
      return static::create();
2880
    }
2881
2882 1
    $offset = $this->indexOfLast($separator);
2883 1
    if ($offset === false) {
2884 1
      return static::create('', $this->encoding);
2885
    }
2886
2887 1
    return static::create(
2888 1
        UTF8::substr(
2889 1
            $this->str,
2890 1
            0,
2891 1
            $offset,
2892 1
            $this->encoding
2893
        ),
2894 1
        $this->encoding
2895
    );
2896
  }
2897
2898
  /**
2899
   * Gets the substring before the last occurrence of a separator.
2900
   * If no match is found returns new empty Stringy object.
2901
   *
2902
   * @param string $separator
2903
   *
2904
   * @return static
2905
   */
2906 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...
2907
  {
2908 1
    if ($separator === '') {
2909
      return static::create();
2910
    }
2911
2912 1
    if ($this->str === '') {
2913 1
      return static::create();
2914
    }
2915
2916 1
    $offset = $this->indexOfLastIgnoreCase($separator);
2917 1
    if ($offset === false) {
2918 1
      return static::create('', $this->encoding);
2919
    }
2920
2921 1
    return static::create(
2922 1
        UTF8::substr(
2923 1
            $this->str,
2924 1
            0,
2925 1
            $offset,
2926 1
            $this->encoding
2927
        ),
2928 1
        $this->encoding
2929
    );
2930
  }
2931
2932
  /**
2933
   * Returns the string with the first letter of each word capitalized,
2934
   * except for when the word is a name which shouldn't be capitalized.
2935
   *
2936
   * @return static <p>Object with $str capitalized.</p>
2937
   */
2938 39
  public function capitalizePersonalName(): self
2939
  {
2940 39
    $stringy = $this->collapseWhitespace();
2941 39
    $stringy->str = $this->capitalizePersonalNameByDelimiter($stringy->str, ' ')->toString();
2942 39
    $stringy->str = $this->capitalizePersonalNameByDelimiter($stringy->str, '-')->toString();
2943
2944 39
    return static::create($stringy, $this->encoding);
2945
  }
2946
2947
  /**
2948
   * @param string $word
2949
   *
2950
   * @return static <p>Object with $str capitalized.</p>
2951
   */
2952 7
  protected function capitalizeWord(string $word): self
2953
  {
2954 7
    $encoding = $this->encoding;
2955
2956 7
    $firstCharacter = UTF8::substr($word, 0, 1, $encoding);
2957 7
    $restOfWord = UTF8::substr($word, 1, null, $encoding);
2958 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 2956 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...
2959
2960 7
    return static::create($firstCharacterUppercased . $restOfWord, $encoding);
2961
  }
2962
2963
  /**
2964
   * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
2965
   *
2966
   * @param string $names
2967
   * @param string $delimiter
2968
   *
2969
   * @return static
2970
   */
2971 39
  protected function capitalizePersonalNameByDelimiter(string $names, string $delimiter): self
2972
  {
2973
    // init
2974 39
    $namesArray = \explode($delimiter, $names);
2975 39
    $encoding = $this->encoding;
2976
2977
    $specialCases = [
2978 39
        'names'    => [
2979
            'ab',
2980
            'af',
2981
            'al',
2982
            'and',
2983
            'ap',
2984
            'bint',
2985
            'binte',
2986
            'da',
2987
            'de',
2988
            'del',
2989
            'den',
2990
            'der',
2991
            'di',
2992
            'dit',
2993
            'ibn',
2994
            'la',
2995
            'mac',
2996
            'nic',
2997
            'of',
2998
            'ter',
2999
            'the',
3000
            'und',
3001
            'van',
3002
            'von',
3003
            'y',
3004
            'zu',
3005
        ],
3006
        'prefixes' => [
3007
            'al-',
3008
            "d'",
3009
            'ff',
3010
            "l'",
3011
            'mac',
3012
            'mc',
3013
            'nic',
3014
        ],
3015
    ];
3016
3017 39
    foreach ($namesArray as &$name) {
3018 39
      if (\in_array($name, $specialCases['names'], true)) {
3019 27
        continue;
3020
      }
3021
3022 13
      $continue = false;
3023
3024 13
      if ($delimiter == '-') {
3025 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...
3026 13
          if (UTF8::strpos($name, $beginning, 0, $encoding) === 0) {
3027 13
            $continue = true;
3028
          }
3029
        }
3030
      }
3031
3032 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...
3033 13
        if (UTF8::strpos($name, $beginning, 0, $encoding) === 0) {
3034 13
          $continue = true;
3035
        }
3036
      }
3037
3038 13
      if ($continue) {
3039 7
        continue;
3040
      }
3041
3042 7
      $name = $this->capitalizeWord($name);
3043
    }
3044
3045 39
    return static::create(\implode($delimiter, $namesArray), $encoding);
3046
  }
3047
}
3048