Completed
Push — master ( ba1955...bbca8d )
by Lars
02:07
created

Stringy::lastSubstringOfIgnoreCase()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 12
Ratio 100 %

Code Coverage

Tests 1
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 2
dl 12
loc 12
ccs 1
cts 1
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Stringy;
4
5
use voku\helper\AntiXSS;
6
use voku\helper\EmailCheck;
7
use voku\helper\URLify;
8
use voku\helper\UTF8;
9
10
/**
11
 * Class Stringy
12
 *
13
 * @package Stringy
14
 */
15
class Stringy implements \Countable, \IteratorAggregate, \ArrayAccess
16
{
17
  /**
18
   * An instance's string.
19
   *
20
   * @var string
21
   */
22
  protected $str;
23
24
  /**
25
   * The string's encoding, which should be one of the mbstring module's
26
   * supported encodings.
27
   *
28
   * @var string
29
   */
30
  protected $encoding;
31
32
  /**
33
   * Initializes a Stringy object and assigns both str and encoding properties
34
   * the supplied values. $str is cast to a string prior to assignment, and if
35
   * $encoding is not specified, it defaults to mb_internal_encoding(). Throws
36
   * an InvalidArgumentException if the first argument is an array or object
37
   * without a __toString method.
38
   *
39
   * @param  mixed  $str      Value to modify, after being cast to string
40
   * @param  string $encoding The character encoding
41
   *
42
   * @throws \InvalidArgumentException if an array or object without a
43
   *         __toString method is passed as the first argument
44
   */
45 1086
  public function __construct($str = '', $encoding = null)
46
  {
47 1086
    if (is_array($str)) {
48 1
      throw new \InvalidArgumentException(
49
          'Passed value cannot be an array'
50 1
      );
51 1085
    }
52 1
53
    if (
54 1
        is_object($str)
55
        &&
56
        !method_exists($str, '__toString')
57
    ) {
58 1084
      throw new \InvalidArgumentException(
59
          'Passed object must have a __toString method'
60
      );
61
    }
62
63 1084
    // don't throw a notice on PHP 5.3
64
    if (!defined('ENT_SUBSTITUTE')) {
65 1084
      define('ENT_SUBSTITUTE', 8);
66
    }
67 1084
68 850
    // init
69 850
    UTF8::checkForSupport();
70 712
71 712
    $this->str = (string)$str;
72
73
    if ($encoding) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encoding of type string|null 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...
74 1084
      $this->encoding = $encoding;
75 850
    } else {
76 850
      UTF8::mbstring_loaded();
77 712
      $this->encoding = mb_internal_encoding();
78
    }
79 1084
80
    if ($encoding) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $encoding of type string|null 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...
81
      $this->encoding = $encoding;
82
    } else {
83
      $this->encoding = mb_internal_encoding();
84
    }
85
  }
86 163
87
  /**
88 163
   * Returns the value in $str.
89
   *
90
   * @return string The current value of the $str property
91
   */
92
  public function __toString()
93
  {
94
    return (string)$this->str;
95
  }
96
97
  /**
98 5
   * Returns a new string with $string appended.
99
   *
100 5
   * @param  string $string The string to append
101
   *
102
   * @return static  Object with appended $string
103
   */
104
  public function append($string)
105
  {
106
    return static::create($this->str . $string, $this->encoding);
107
  }
108
109
  /**
110 1
   * Append an password (limited to chars that are good readable).
111
   *
112 1
   * @param int $length length of the random string
113
   *
114 1
   * @return static  Object with appended password
115
   */
116
  public function appendPassword($length)
117
  {
118
    $possibleChars = '2346789bcdfghjkmnpqrtvwxyzBCDFGHJKLMNPQRTVWXYZ';
119
120
    return $this->appendRandomString($length, $possibleChars);
121
  }
122
123
  /**
124 1
   * Append an unique identifier.
125
   *
126 1
   * @param string|int $extraPrefix
127 1
   *
128 1
   * @return static  Object with appended unique identifier as md5-hash
129 1
   */
130 1
  public function appendUniqueIdentifier($extraPrefix = '')
0 ignored issues
show
Coding Style introduced by
appendUniqueIdentifier uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
131
  {
132 1
    $prefix = mt_rand() .
133
              session_id() .
134
              (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '') .
135
              (isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '') .
136
              $extraPrefix;
137
138
    return $this->append(md5(uniqid($prefix, true) . $prefix));
139
  }
140
141
  /**
142
   * Append an random string.
143 2
   *
144
   * @param int    $length        length of the random string
145
   * @param string $possibleChars characters string for the random selection
146 2
   *
147 2
   * @return static  Object with appended random string
148 2
   */
149 2
  public function appendRandomString($length, $possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
150
  {
151 2
    // init
152 1
    $i = 0;
153
    $length = (int)$length;
154
    $str = $this->str;
155
    $maxlength = UTF8::strlen($possibleChars, $this->encoding);
156 2
157 2
    if ($maxlength === 0) {
158 2
      return $this;
159 2
    }
160 2
161
    // add random chars
162 2
    while ($i < $length) {
163
      $char = UTF8::substr($possibleChars, mt_rand(0, $maxlength - 1), 1, $this->encoding);
164
      $str .= $char;
165
      $i++;
166
    }
167
168
    return $this->append($str);
169
  }
170
171
  /**
172
   * Creates a Stringy object and assigns both str and encoding properties
173
   * the supplied values. $str is cast to a string prior to assignment, and if
174
   * $encoding is not specified, it defaults to mb_internal_encoding(). It
175
   * then returns the initialized object. Throws an InvalidArgumentException
176
   * if the first argument is an array or object without a __toString method.
177
   *
178
   * @param  mixed  $str      Value to modify, after being cast to string
179 1076
   * @param  string $encoding The character encoding
180
   *
181 1076
   * @return static  A Stringy object
182
   * @throws \InvalidArgumentException if an array or object without a
183
   *         __toString method is passed as the first argument
184
   */
185
  public static function create($str = '', $encoding = null)
186
  {
187
    return new static($str, $encoding);
188
  }
189
190
  /**
191
   * Returns the substring between $start and $end, if found, or an empty
192
   * string. An optional offset may be supplied from which to begin the
193
   * search for the start string.
194
   *
195 16
   * @param  string $start  Delimiter marking the start of the substring
196
   * @param  string $end    Delimiter marking the end of the substring
197 16
   * @param  int    $offset Index from which to begin the search
198 16
   *
199 2
   * @return static  Object whose $str is a substring between $start and $end
200
   */
201
  public function between($start, $end, $offset = 0)
202 14
  {
203 14
    $startIndex = $this->indexOf($start, $offset);
204 14
    if ($startIndex === false) {
205 2
      return static::create('', $this->encoding);
206
    }
207
208 12
    $substrIndex = $startIndex + UTF8::strlen($start, $this->encoding);
209
    $endIndex = $this->indexOf($end, $substrIndex);
210
    if ($endIndex === false) {
211
      return static::create('', $this->encoding);
212
    }
213
214
    return $this->substr($substrIndex, $endIndex - $substrIndex);
215
  }
216
217
  /**
218
   * Returns the index of the first occurrence of $needle in the string,
219
   * and false if not found. Accepts an optional offset from which to begin
220
   * the search.
221 28
   *
222
   * @param  string $needle Substring to look for
223 28
   * @param  int    $offset Offset from which to search
224
   *
225
   * @return int|bool The occurrence's index if found, otherwise false
226
   */
227
  public function indexOf($needle, $offset = 0)
228
  {
229
    return UTF8::strpos($this->str, (string)$needle, (int)$offset, $this->encoding);
230
  }
231
232
  /**
233
   * Returns the index of the first occurrence of $needle in the string,
234
   * and false if not found. Accepts an optional offset from which to begin
235
   * the search.
236 64
   *
237
   * @param  string $needle Substring to look for
238 64
   * @param  int    $offset Offset from which to search
239 19
   *
240 19
   * @return int|bool The occurrence's index if found, otherwise false
241
   */
242 64
  public function indexOfIgnoreCase($needle, $offset = 0)
243
  {
244 64
    return UTF8::stripos($this->str, (string)$needle, (int)$offset, $this->encoding);
245
  }
246
247
  /**
248
   * Returns the substring beginning at $start with the specified $length.
249
   * It differs from the UTF8::substr() function in that providing a $length of
250
   * null will return the rest of the string, rather than an empty string.
251
   *
252 248
   * @param  int $start  Position of the first character to use
253
   * @param  int $length Maximum number of characters used
254 248
   *
255
   * @return static  Object with its $str being the substring
256
   */
257
  public function substr($start, $length = null)
258
  {
259
    if ($length === null) {
260
      $length = $this->length();
261
    }
262
263
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
264 52
265
    return static::create($str, $this->encoding);
266 52
  }
267
268
  /**
269
   * Returns the length of the string.
270
   *
271
   * @return int The number of characters in $str given the encoding
272
   */
273
  public function length()
274
  {
275
    return UTF8::strlen($this->str, $this->encoding);
276
  }
277
278 153
  /**
279
   * Trims the string and replaces consecutive whitespace characters with a
280 153
   * single space. This includes tabs and newline characters, as well as
281 152
   * multibyte whitespace such as the thin space and ideographic space.
282 152
   *
283 1
   * @return static  Object with a trimmed $str and condensed whitespace
284
   */
285
  public function collapseWhitespace()
286 153
  {
287
    return $this->regexReplace('[[:space:]]+', ' ')->trim();
288
  }
289
290
  /**
291
   * Returns a string with whitespace removed from the start and end of the
292
   * string. Supports the removal of unicode whitespace. Accepts an optional
293
   * string of characters to strip instead of the defaults.
294
   *
295
   * @param  string $chars Optional string of characters to strip
296
   *
297
   * @return static  Object with a trimmed $str
298 223
   */
299 View Code Duplication
  public function trim($chars = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
300 223
  {
301 8
    if (!$chars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chars of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
302 8
      $chars = '[:space:]';
303
    } else {
304 223
      $chars = preg_quote($chars, '/');
305 223
    }
306 223
307 223
    return $this->regexReplace("^[$chars]+|[$chars]+\$", '');
308 223
  }
309
310 223
  /**
311
   * Replaces all occurrences of $pattern in $str by $replacement.
312
   *
313
   * @param  string $pattern     The regular expression pattern
314
   * @param  string $replacement The string to replace with
315
   * @param  string $options     Matching conditions to be used
316
   *
317
   * @return static  Object with the result2ing $str after the replacements
318
   */
319
  public function regexReplace($pattern, $replacement, $options = '')
320
  {
321
    if ($options === 'msr') {
322
      $options = 'ms';
323 43
    }
324
325
    $str = preg_replace(
326 43
        '/' . $pattern . '/u' . $options,
327 1
        $replacement,
328
        $this->str
329
    );
330 42
331 42
    return static::create($str, $this->encoding);
332 18
  }
333
334 24
  /**
335
   * Returns true if the string contains all $needles, false otherwise. By
336 24
   * default the comparison is case-sensitive, but can be made insensitive by
337
   * setting $caseSensitive to false.
338
   *
339
   * @param  array $needles       SubStrings to look for
340
   * @param  bool  $caseSensitive Whether or not to enforce case-sensitivity
341
   *
342
   * @return bool   Whether or not $str contains $needle
343
   */
344 View Code Duplication
  public function containsAll($needles, $caseSensitive = true)
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...
345
  {
346
    /** @noinspection IsEmptyFunctionUsageInspection */
347
    if (empty($needles)) {
348
      return false;
349 105
    }
350
351 105
    foreach ($needles as $needle) {
352
      if (!$this->contains($needle, $caseSensitive)) {
353 105
        return false;
354 55
      }
355
    }
356 50
357
    return true;
358
  }
359
360
  /**
361
   * Returns true if the string contains $needle, false otherwise. By default
362
   * the comparison is case-sensitive, but can be made insensitive by setting
363
   * $caseSensitive to false.
364
   *
365
   * @param  string $needle        Substring to look for
366
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
367
   *
368
   * @return bool   Whether or not $str contains $needle
369
   */
370 43
  public function contains($needle, $caseSensitive = true)
371
  {
372
    $encoding = $this->encoding;
373 43
374 1
    if ($caseSensitive) {
375
      return (UTF8::strpos($this->str, $needle, 0, $encoding) !== false);
376
    }
377 42
378 42
    return (UTF8::stripos($this->str, $needle, 0, $encoding) !== false);
379 24
  }
380
381 18
  /**
382
   * Returns true if the string contains any $needles, false otherwise. By
383 18
   * default the comparison is case-sensitive, but can be made insensitive by
384
   * setting $caseSensitive to false.
385
   *
386
   * @param  array $needles       SubStrings to look for
387
   * @param  bool  $caseSensitive Whether or not to enforce case-sensitivity
388
   *
389
   * @return bool   Whether or not $str contains $needle
390
   */
391 1 View Code Duplication
  public function containsAny($needles, $caseSensitive = true)
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...
392
  {
393 1
    /** @noinspection IsEmptyFunctionUsageInspection */
394
    if (empty($needles)) {
395
      return false;
396
    }
397
398
    foreach ($needles as $needle) {
399
      if ($this->contains($needle, $caseSensitive)) {
400
        return true;
401
      }
402
    }
403
404
    return false;
405
  }
406 15
407
  /**
408 15
   * Returns the length of the string, implementing the countable interface.
409 9
   *
410
   * @return int The number of characters in the string, given the encoding
411
   */
412 6
  public function count()
413 6
  {
414
    return $this->length();
415 6
  }
416
417
  /**
418
   * Returns the number of occurrences of $substring in the given string.
419
   * By default, the comparison is case-sensitive, but can be made insensitive
420
   * by setting $caseSensitive to false.
421
   *
422
   * @param  string $substring     The substring to search for
423
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
424
   *
425 19
   * @return int    The number of $substring occurrences
426
   */
427 19
  public function countSubstr($substring, $caseSensitive = true)
428
  {
429
    if ($caseSensitive) {
430
      return UTF8::substr_count($this->str, $substring, 0, null, $this->encoding);
431
    }
432
433
    $str = UTF8::strtoupper($this->str, $this->encoding);
434
    $substring = UTF8::strtoupper($substring, $this->encoding);
435
436
    return UTF8::substr_count($str, $substring, 0, null, $this->encoding);
437
  }
438
439
  /**
440 49
   * Returns a lowercase and trimmed string separated by dashes. Dashes are
441
   * inserted before uppercase characters (with the exception of the first
442 49
   * character of the string), and in place of spaces as well as underscores.
443
   *
444 49
   * @return static  Object with a dasherized $str
445
   */
446 49
  public function dasherize()
447
  {
448 49
    return $this->delimit('-');
449
  }
450 49
451
  /**
452
   * Returns a lowercase and trimmed string separated by the given delimiter.
453
   * Delimiters are inserted before uppercase characters (with the exception
454
   * of the first character of the string), and in place of spaces, dashes,
455
   * and underscores. Alpha delimiters are not converted to lowercase.
456
   *
457
   * @param  string $delimiter Sequence used to separate parts of the string
458
   *
459
   * @return static  Object with a delimited $str
460
   */
461 10
  public function delimit($delimiter)
462
  {
463 10
    $str = $this->trim();
464
465 10
    $str = preg_replace('/\B([A-Z])/u', '-\1', $str);
466 4
467 4
    $str = UTF8::strtolower($str, $this->encoding);
0 ignored issues
show
Bug introduced by
It seems like $str can also be of type array<integer,string>; however, voku\helper\UTF8::strtolower() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
468
469 10
    $str = preg_replace('/[-_\s]+/u', $delimiter, $str);
470
471
    return static::create($str, $this->encoding);
472
  }
473
474
  /**
475
   * Ensures that the string begins with $substring. If it doesn't, it's
476
   * prepended.
477
   *
478
   * @param  string $substring The substring to add if not present
479
   *
480
   * @return static  Object with its $str prefixed by the $substring
481
   */
482 45 View Code Duplication
  public function ensureLeft($substring)
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...
483
  {
484 45
    $stringy = static::create($this->str, $this->encoding);
485
486 45
    if (!$stringy->startsWith($substring)) {
487 8
      $stringy->str = $substring . $stringy->str;
488 8
    }
489 8
490
    return $stringy;
491 45
  }
492
493
  /**
494
   * Returns true if the string begins with $substring, false otherwise. By
495
   * default, the comparison is case-sensitive, but can be made insensitive
496
   * by setting $caseSensitive to false.
497
   *
498
   * @param  string $substring     The substring to look for
499
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
500
   *
501
   * @return bool   Whether or not $str starts with $substring
502
   */
503
  public function startsWith($substring, $caseSensitive = true)
504 12
  {
505
    $str = $this->str;
506 12
507 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...
508
      $substring = UTF8::strtolower($substring, $this->encoding);
509
      $str = UTF8::strtolower($this->str, $this->encoding);
510 12
    }
511 12
512 6
    return UTF8::strpos($str, $substring, $this->encoding) === 0;
513
  }
514 6
515
  /**
516 6
   * Returns true if the string begins with any of $substrings, false otherwise.
517
   * By default the comparison is case-sensitive, but can be made insensitive by
518
   * setting $caseSensitive to false.
519
   *
520
   * @param  array $substrings    Substrings to look for
521
   * @param  bool  $caseSensitive Whether or not to enforce case-sensitivity
522
   *
523
   * @return bool   Whether or not $str starts with $substring
524
   */
525 View Code Duplication
  public function startsWithAny(array $substrings, $caseSensitive = true)
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...
526
  {
527 10
    if (empty($substrings)) {
528
      return false;
529 10
    }
530
531 10
    foreach ($substrings as $substring) {
532 4
      if ($this->startsWith($substring, $caseSensitive)) {
533 4
        return true;
534
      }
535 10
    }
536
537
    return false;
538
  }
539
540
  /**
541
   * Ensures that the string ends with $substring. If it doesn't, it's
542
   * appended.
543
   *
544
   * @param  string $substring The substring to add if not present
545
   *
546
   * @return static  Object with its $str suffixed by the $substring
547
   */
548 33 View Code Duplication
  public function ensureRight($substring)
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...
549
  {
550 33
    $stringy = static::create($this->str, $this->encoding);
551 33
552
    if (!$stringy->endsWith($substring)) {
553 33
      $stringy->str .= $substring;
554 33
    }
555 33
556 33
    return $stringy;
557 33
  }
558 33
559
  /**
560 33
   * Returns true if the string ends with $substring, false otherwise. By
561 4
   * default, the comparison is case-sensitive, but can be made insensitive
562 4
   * by setting $caseSensitive to false.
563 4
   *
564
   * @param  string $substring     The substring to look for
565 33
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
566
   *
567
   * @return bool   Whether or not $str ends with $substring
568
   */
569
  public function endsWith($substring, $caseSensitive = true)
570
  {
571
    $substringLength = UTF8::strlen($substring, $this->encoding);
572
    $strLength = $this->length();
573
574
    $endOfStr = UTF8::substr(
575 12
        $this->str,
576
        $strLength - $substringLength,
577 12
        $substringLength,
578
        $this->encoding
579 12
    );
580 2
581 2 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...
582 10
      $substring = UTF8::strtolower($substring, $this->encoding);
583
      $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...
584
    }
585 2
586
    return (string)$substring === $endOfStr;
587
  }
588
589
  /**
590
   * Returns true if the string ends with any of $substrings, false otherwise.
591
   * By default, the comparison is case-sensitive, but can be made insensitive
592
   * by setting $caseSensitive to false.
593 3
   *
594
   * @param  string[] $substrings    Substrings to look for
595 3
   * @param  bool     $caseSensitive Whether or not to enforce
596
   *                                 case-sensitivity
597
   *
598
   * @return bool     Whether or not $str ends with $substring
599
   */
600 View Code Duplication
  public function endsWithAny($substrings, $caseSensitive = true)
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...
601
  {
602
    if (empty($substrings)) {
603
      return false;
604
    }
605
606 1
    foreach ($substrings as $substring) {
607
      if ($this->endsWith($substring, $caseSensitive)) {
608 1
        return true;
609
      }
610
    }
611
612
    return false;
613
  }
614
615
  /**
616 4
   * Returns the first $n characters of the string.
617
   *
618
   * @param  int $n Number of characters to retrieve from the start
619 4
   *
620 4
   * @return static  Object with its $str being the first $n chars
621
   */
622 4 View Code Duplication
  public function first($n)
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...
623 3
  {
624 3
    $stringy = static::create($this->str, $this->encoding);
625
626 4
    if ($n < 0) {
627
      $stringy->str = '';
628
    } else {
629
      return $stringy->substr(0, $n);
630
    }
631
632
    return $stringy;
633
  }
634
635
  /**
636 11
   * Returns the encoding used by the Stringy object.
637
   *
638 11
   * @return string The current value of the $encoding property
639
   */
640
  public function getEncoding()
641
  {
642
    return $this->encoding;
643
  }
644
645
  /**
646
   * Returns a new ArrayIterator, thus implementing the IteratorAggregate
647 12
   * interface. The ArrayIterator's constructor is passed an array of chars
648
   * in the multibyte string. This enables the use of foreach with instances
649 12
   * of Stringy\Stringy.
650
   *
651
   * @return \ArrayIterator An iterator for the characters in the string
652
   */
653
  public function getIterator()
654
  {
655
    return new \ArrayIterator($this->chars());
656
  }
657
658
  /**
659 103
   * Returns an array consisting of the characters in the string.
660
   *
661 103
   * @return array An array of string chars
662 64
   */
663
  public function chars()
664 39
  {
665
    // init
666
    $chars = array();
667
    $l = $this->length();
668
669
    for ($i = 0; $i < $l; $i++) {
670
      $chars[] = $this->at($i)->str;
671
    }
672
673
    return $chars;
674 12
  }
675
676 12
  /**
677
   * Returns the character at $index, with indexes starting at 0.
678
   *
679
   * @param  int $index Position of the character
680
   *
681
   * @return static  The character at $index
682
   */
683
  public function at($index)
684
  {
685
    return $this->substr($index, 1);
686 5
  }
687
688 5
  /**
689
   * Returns true if the string contains a lower case char, false
690 5
   * otherwise.
691
   *
692
   * @return bool Whether or not the string contains a lower case character.
693
   */
694
  public function hasLowerCase()
695
  {
696
    return $this->matchesPattern('.*[[:lower:]]');
697
  }
698
699
  /**
700 5
   * Returns true if $str matches the supplied pattern, false otherwise.
701
   *
702 5
   * @param  string $pattern Regex pattern to match against
703
   *
704 5
   * @return bool   Whether or not $str matches the pattern
705
   */
706
  protected function matchesPattern($pattern)
707
  {
708
    if (preg_match('/' . $pattern . '/u', $this->str)) {
709
      return true;
710
    }
711
712
    return false;
713 3
  }
714
715 3
  /**
716
   * Returns true if the string contains an upper case char, false
717 3
   * otherwise.
718
   *
719
   * @return bool Whether or not the string contains an upper case character.
720
   */
721
  public function hasUpperCase()
722
  {
723
    return $this->matchesPattern('.*[[:upper:]]');
724
  }
725 27
726
  /**
727 27
   * Convert all HTML entities to their applicable characters.
728 27
   *
729 27
   * @param  int|null $flags Optional flags
730 27
   *
731 27
   * @return static   Object with the resulting $str after being html decoded.
732 27
   */
733 27
  public function htmlDecode($flags = ENT_COMPAT)
734
  {
735 27
    $str = UTF8::html_entity_decode($this->str, $flags, $this->encoding);
736
737 27
    return static::create($str, $this->encoding);
738
  }
739
740
  /**
741
   * Convert all applicable characters to HTML entities.
742
   *
743
   * @param  int|null $flags Optional flags
744
   *
745
   * @return static   Object with the resulting $str after being html encoded.
746
   */
747
  public function htmlEncode($flags = ENT_COMPAT)
748
  {
749
    $str = UTF8::htmlentities($this->str, $flags, $this->encoding);
750
751 12
    return static::create($str, $this->encoding);
752
  }
753 12
754
  /**
755
   * Capitalizes the first word of the string, replaces underscores with
756
   * spaces, and strips '_id'.
757
   *
758
   * @return static  Object with a humanized $str
759
   */
760
  public function humanize()
761
  {
762
    $str = UTF8::str_replace(array('_id', '_'), array('', ' '), $this->str);
763
764 8
    return static::create($str, $this->encoding)->trim()->upperCaseFirst();
765
  }
766 8
767 8
  /**
768 1
   * Converts the first character of the supplied string to upper case.
769
   *
770
   * @return static  Object with the first character of $str being upper case
771 7
   */
772 7 View Code Duplication
  public function upperCaseFirst()
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...
773
  {
774 7
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
775
    $rest = UTF8::substr(
776 7
        $this->str,
777
        1,
778
        $this->length() - 1,
779
        $this->encoding
780
    );
781
782
    $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 774 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...
783
784
    return static::create($str, $this->encoding);
785
  }
786
787
  /**
788
   * Returns the index of the last occurrence of $needle in the string,
789
   * and false if not found. Accepts an optional offset from which to begin
790
   * the search. Offsets may be negative to count from the last character
791 13
   * in the string.
792
   *
793 13
   * @param  string $needle Substring to look for
794 1
   * @param  int    $offset Offset from which to search
795
   *
796
   * @return int|bool The last occurrence's index if found, otherwise false
797 12
   */
798 12
  public function indexOfLast($needle, $offset = 0)
799
  {
800 12
    return UTF8::strrpos($this->str, (string)$needle, (int)$offset, $this->encoding);
801
  }
802
803
  /**
804
   * Returns the index of the last occurrence of $needle in the string,
805
   * and false if not found. Accepts an optional offset from which to begin
806
   * the search. Offsets may be negative to count from the last character
807
   * in the string.
808
   *
809 10
   * @param  string $needle Substring to look for
810
   * @param  int    $offset Offset from which to search
811 10
   *
812
   * @return int|bool The last occurrence's index if found, otherwise false
813
   */
814
  public function indexOfLastIgnoreCase($needle, $offset = 0)
815
  {
816
    return UTF8::strripos($this->str, (string)$needle, (int)$offset, $this->encoding);
817
  }
818
819
  /**
820
   * Inserts $substring into the string at the $index provided.
821
   *
822
   * @param  string $substring String to be inserted
823
   * @param  int    $index     The index at which to insert the substring
824
   *
825
   * @return static  Object with the resulting $str after the insertion
826
   */
827 View Code Duplication
  public function insert($substring, $index)
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...
828
  {
829
    $stringy = static::create($this->str, $this->encoding);
830
    if ($index > $stringy->length()) {
831
      return $stringy;
832
    }
833 13
834
    $start = UTF8::substr($stringy->str, 0, $index, $stringy->encoding);
835 13
    $end = UTF8::substr($stringy->str, $index, $stringy->length(), $stringy->encoding);
836
837
    $stringy->str = $start . $substring . $end;
838
839
    return $stringy;
840
  }
841
842
  /**
843
   * Returns true if the string contains the $pattern, otherwise false.
844 15
   *
845
   * WARNING: Asterisks ("*") are translated into (".*") zero-or-more regular
846 15
   * expression wildcards.
847
   *
848
   * @credit Originally from Laravel, thanks Taylor.
849
   *
850
   * @param string $pattern The string or pattern to match against.
851
   *
852
   * @return bool Whether or not we match the provided pattern.
853
   */
854
  public function is($pattern)
855 13
  {
856
    if ($this->toString() === $pattern) {
857 13
      return true;
858
    }
859
860
    $quotedPattern = preg_quote($pattern, '/');
861
    $replaceWildCards = str_replace('\*', '.*', $quotedPattern);
862
863
    return $this->matchesPattern('^' . $replaceWildCards . '\z');
864
  }
865 1
866
  /**
867 1
   * Returns true if the string contains only alphabetic chars, false
868
   * otherwise.
869
   *
870
   * @return bool Whether or not $str contains only alphabetic chars
871
   */
872
  public function isAlpha()
873
  {
874
    return $this->matchesPattern('^[[:alpha:]]*$');
875
  }
876
877
  /**
878
   * Determine whether the string is considered to be empty.
879
   *
880 1
   * A variable is considered empty if it does not exist or if its value equals FALSE.
881
   * empty() does not generate a warning if the variable does not exist.
882 1
   *
883
   * @return bool
884
   */
885
  public function isEmpty()
886
  {
887
    return empty($this->str);
888
  }
889
890
  /**
891
   * Returns true if the string contains only alphabetic and numeric chars,
892 20
   * false otherwise.
893
   *
894 20
   * @return bool Whether or not $str contains only alphanumeric chars
895 1
   */
896
  public function isAlphanumeric()
897
  {
898 19
    return $this->matchesPattern('^[[:alnum:]]*$');
899
  }
900 19
901 11
  /**
902
   * Returns true if the string contains only whitespace chars, false
903 8
   * otherwise.
904
   *
905
   * @return bool Whether or not $str contains only whitespace characters
906
   */
907
  public function isBlank()
908
  {
909
    return $this->matchesPattern('^[[:space:]]*$');
910
  }
911
912
  /**
913 8
   * Returns true if the string contains only hexadecimal chars, false
914
   * otherwise.
915 8
   *
916 3
   * @return bool Whether or not $str contains only hexadecimal chars
917
   */
918 5
  public function isHexadecimal()
919
  {
920
    return $this->matchesPattern('^[[:xdigit:]]*$');
921
  }
922
923
  /**
924
   * Returns true if the string contains HTML-Tags, false otherwise.
925
   *
926
   * @return bool Whether or not $str contains HTML-Tags
927 7
   */
928
  public function isHtml()
929 7
  {
930 1
    return UTF8::is_html($this->str);
931
  }
932
933
  /**
934
   * Returns true if the string contains a valid E-Mail address, false otherwise.
935 6
   *
936 6
   * @param bool $useExampleDomainCheck
937 6
   * @param bool $useTypoInDomainCheck
938 6
   * @param bool $useTemporaryDomainCheck
939 4
   * @param bool $useDnsCheck
940
   *
941 2
   * @return bool Whether or not $str contains a valid E-Mail address
942
   */
943
  public function isEmail($useExampleDomainCheck = false, $useTypoInDomainCheck = false, $useTemporaryDomainCheck = false, $useDnsCheck = false)
944
  {
945
    return EmailCheck::isValid($this->str, $useExampleDomainCheck, $useTypoInDomainCheck, $useTemporaryDomainCheck, $useDnsCheck);
946
  }
947
948
  /**
949
   * Returns true if the string is JSON, false otherwise. Unlike json_decode
950
   * in PHP 5.x, this method is consistent with PHP 7 and other JSON parsers,
951 8
   * in that an empty string is not considered valid JSON.
952
   *
953 8
   * @return bool Whether or not $str is JSON
954
   */
955
  public function isJson()
956
  {
957
    if (!isset($this->str[0])) {
958
      return false;
959
    }
960
961
    json_decode($this->str);
962
963 12
    if (json_last_error() === JSON_ERROR_NONE) {
964
      return true;
965 12
    }
966
967 12
    return false;
968 4
  }
969 4
970 8
  /**
971
   * Returns true if the string contains only lower case chars, false
972
   * otherwise.
973 4
   *
974
   * @return bool Whether or not $str contains only lower case characters
975
   */
976
  public function isLowerCase()
977
  {
978
    if ($this->matchesPattern('^[[:lower:]]*$')) {
979
      return true;
980
    }
981
982 15
    return false;
983
  }
984 15
985
  /**
986
   * Returns true if the string is serialized, false otherwise.
987 15
   *
988 15
   * @return bool Whether or not $str is serialized
989 15
   */
990
  public function isSerialized()
991 15
  {
992
    if (!isset($this->str[0])) {
993
      return false;
994
    }
995
996
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
997
    if (
998
        $this->str === 'b:0;'
999
        ||
1000
        @unserialize($this->str) !== false
1001 10
    ) {
1002
      return true;
1003 10
    }
1004 10
1005
    return false;
1006 10
  }
1007 10
1008 8
  /**
1009
   * Returns true if the string contains only lower case chars, false
1010 8
   * otherwise.
1011 6
   *
1012 6
   * @return bool Whether or not $str contains only lower case characters
1013 6
   */
1014
  public function isUpperCase()
1015 6
  {
1016
    return $this->matchesPattern('^[[:upper:]]*$');
1017 10
  }
1018
1019
  /**
1020
   * Returns the last $n characters of the string.
1021
   *
1022
   * @param  int $n Number of characters to retrieve from the end
1023
   *
1024
   * @return static  Object with its $str being the last $n chars
1025
   */
1026 View Code Duplication
  public function last($n)
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...
1027 10
  {
1028
    $stringy = static::create($this->str, $this->encoding);
1029 10
1030 10
    if ($n <= 0) {
1031
      $stringy->str = '';
1032 10
    } else {
1033 10
      return $stringy->substr(-$n);
1034 8
    }
1035
1036 8
    return $stringy;
1037 6
  }
1038 6
1039 6
  /**
1040
   * Splits on newlines and carriage returns, returning an array of Stringy
1041 6
   * objects corresponding to the lines in the string.
1042
   *
1043 10
   * @return static [] An array of Stringy objects
1044
   */
1045
  public function lines()
1046
  {
1047
    $array = preg_split('/[\r\n]{1,2}/u', $this->str);
1048
    /** @noinspection CallableInLoopTerminationConditionInspection */
1049
    /** @noinspection ForeachInvariantsInspection */
1050 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...
1051
      $array[$i] = static::create($array[$i], $this->encoding);
1052
    }
1053
1054 10
    return $array;
1055
  }
1056
1057
  /**
1058 10
   * Returns the longest common prefix between the string and $otherStr.
1059 10
   *
1060 10
   * @param  string $otherStr Second string for comparison
1061 10
   *
1062
   * @return static  Object with its $str being the longest common prefix
1063
   */
1064 10
  public function longestCommonPrefix($otherStr)
1065 2
  {
1066
    $encoding = $this->encoding;
1067 2
    $maxLength = min($this->length(), UTF8::strlen($otherStr, $encoding));
1068
1069
    $longestCommonPrefix = '';
1070 8
    for ($i = 0; $i < $maxLength; $i++) {
1071 8
      $char = UTF8::substr($this->str, $i, 1, $encoding);
1072 8
1073 8
      if ($char == UTF8::substr($otherStr, $i, 1, $encoding)) {
1074 8
        $longestCommonPrefix .= $char;
1075 8
      } else {
1076
        break;
1077 8
      }
1078 8
    }
1079 8
1080 8
    return static::create($longestCommonPrefix, $encoding);
1081
  }
1082 8
1083 8
  /**
1084 8
   * Returns the longest common suffix between the string and $otherStr.
1085 8
   *
1086 8
   * @param  string $otherStr Second string for comparison
1087 8
   *
1088 8
   * @return static  Object with its $str being the longest common suffix
1089 8
   */
1090
  public function longestCommonSuffix($otherStr)
1091 8
  {
1092 8
    $encoding = $this->encoding;
1093
    $maxLength = min($this->length(), UTF8::strlen($otherStr, $encoding));
1094 8
1095
    $longestCommonSuffix = '';
1096 8
    for ($i = 1; $i <= $maxLength; $i++) {
1097
      $char = UTF8::substr($this->str, -$i, 1, $encoding);
1098
1099
      if ($char == UTF8::substr($otherStr, -$i, 1, $encoding)) {
1100
        $longestCommonSuffix = $char . $longestCommonSuffix;
1101
      } else {
1102
        break;
1103
      }
1104
    }
1105
1106
    return static::create($longestCommonSuffix, $encoding);
1107
  }
1108 6
1109
  /**
1110
   * Returns the longest common substring between the string and $otherStr.
1111 6
   * In the case of ties, it returns that which occurs first.
1112 6
   *
1113
   * @param  string $otherStr Second string for comparison
1114 6
   *
1115 3
   * @return static  Object with its $str being the longest common substring
1116
   */
1117
  public function longestCommonSubstring($otherStr)
1118 3
  {
1119
    // Uses dynamic programming to solve
1120
    // http://en.wikipedia.org/wiki/Longest_common_substring_problem
1121
    $encoding = $this->encoding;
1122
    $stringy = static::create($this->str, $encoding);
1123
    $strLength = $stringy->length();
1124
    $otherLength = UTF8::strlen($otherStr, $encoding);
1125
1126
    // Return if either string is empty
1127
    if ($strLength == 0 || $otherLength == 0) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $strLength of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
Bug introduced by
It seems like you are loosely comparing $otherLength of type integer|null to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
1128
      $stringy->str = '';
1129
1130
      return $stringy;
1131
    }
1132
1133 2
    $len = 0;
1134
    $end = 0;
1135
    $table = array_fill(
1136 2
        0, $strLength + 1,
1137 2
        array_fill(0, $otherLength + 1, 0)
1138
    );
1139
1140 2
    for ($i = 1; $i <= $strLength; $i++) {
1141
      for ($j = 1; $j <= $otherLength; $j++) {
1142 1
        $strChar = UTF8::substr($stringy->str, $i - 1, 1, $encoding);
1143 2
        $otherChar = UTF8::substr($otherStr, $j - 1, 1, $encoding);
1144 1
1145
        if ($strChar == $otherChar) {
1146
          $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
1147 1
          if ($table[$i][$j] > $len) {
1148
            $len = $table[$i][$j];
1149
            $end = $i;
1150
          }
1151
        } else {
1152
          $table[$i][$j] = 0;
1153
        }
1154
      }
1155
    }
1156
1157
    $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...
1158
1159 1
    return $stringy;
1160
  }
1161
1162 1
  /**
1163
   * Returns whether or not a character exists at an index. Offsets may be
1164
   * negative to count from the last character in the string. Implements
1165
   * part of the ArrayAccess interface.
1166
   *
1167
   * @param  mixed $offset The index to check
1168
   *
1169
   * @return boolean Whether or not the index exists
1170
   */
1171
  public function offsetExists($offset)
1172
  {
1173 1
    // init
1174
    $length = $this->length();
1175
    $offset = (int)$offset;
1176 1
1177
    if ($offset >= 0) {
1178
      return ($length > $offset);
1179
    }
1180
1181
    return ($length >= abs($offset));
1182
  }
1183
1184
  /**
1185
   * Returns the character at the given index. Offsets may be negative to
1186
   * count from the last character in the string. Implements part of the
1187
   * ArrayAccess interface, and throws an OutOfBoundsException if the index
1188
   * does not exist.
1189
   *
1190
   * @param  mixed $offset The index from which to retrieve the char
1191
   *
1192
   * @return string                 The character at the specified index
1193 13
   * @throws \OutOfBoundsException If the positive or negative offset does
1194
   *                               not exist
1195 13
   */
1196 1
  public function offsetGet($offset)
1197
  {
1198 1
    // init
1199
    $offset = (int)$offset;
1200
    $length = $this->length();
1201
1202 12
    if (
1203 3
        ($offset >= 0 && $length <= $offset)
1204 9
        ||
1205 6
        $length < abs($offset)
1206 3
    ) {
1207 3
      throw new \OutOfBoundsException('No character exists at the index');
1208 3
    }
1209
1210
    return UTF8::substr($this->str, $offset, 1, $this->encoding);
1211
  }
1212
1213
  /**
1214
   * Implements part of the ArrayAccess interface, but throws an exception
1215
   * when called. This maintains the immutability of Stringy objects.
1216
   *
1217
   * @param  mixed $offset The index of the character
1218
   * @param  mixed $value  Value to set
1219
   *
1220 10
   * @throws \Exception When called
1221
   */
1222 10
  public function offsetSet($offset, $value)
1223
  {
1224
    // Stringy is immutable, cannot directly set char
1225
    throw new \Exception('Stringy object is immutable, cannot modify char');
1226
  }
1227
1228
  /**
1229
   * Implements part of the ArrayAccess interface, but throws an exception
1230
   * when called. This maintains the immutability of Stringy objects.
1231
   *
1232
   * @param  mixed $offset The index of the character
1233
   *
1234
   * @throws \Exception When called
1235 37
   */
1236
  public function offsetUnset($offset)
1237 37
  {
1238
    // Don't allow directly modifying the string
1239 37
    throw new \Exception('Stringy object is immutable, cannot unset char');
1240
  }
1241 37
1242 37
  /**
1243
   * Pads the string to a given length with $padStr. If length is less than
1244 37
   * or equal to the length of the string, no padding takes places. The
1245 3
   * default string used for padding is a space, and the default type (one of
1246
   * 'left', 'right', 'both') is 'right'. Throws an InvalidArgumentException
1247
   * if $padType isn't one of those 3 values.
1248 34
   *
1249 34
   * @param  int    $length  Desired string length after padding
1250 34
   * @param  string $padStr  String used to pad, defaults to space
1251 34
   * @param  string $padType One of 'left', 'right', 'both'
1252 34
   *
1253 34
   * @return static  Object with a padded $str
1254 34
   * @throws \InvalidArgumentException If $padType isn't one of 'right', 'left' or 'both'
1255 34
   */
1256 34
  public function pad($length, $padStr = ' ', $padType = 'right')
1257
  {
1258 34
    if (!in_array($padType, array('left', 'right', 'both'), true)) {
1259 34
      throw new \InvalidArgumentException(
1260 34
          'Pad expects $padType ' . "to be one of 'left', 'right' or 'both'"
1261 34
      );
1262 34
    }
1263 34
1264 34
    switch ($padType) {
1265 34
      case 'left':
1266 34
        return $this->padLeft($length, $padStr);
1267
      case 'right':
1268 34
        return $this->padRight($length, $padStr);
1269
      default:
1270 34
        return $this->padBoth($length, $padStr);
1271
    }
1272
  }
1273
1274
  /**
1275
   * Returns a new string of a given length such that the beginning of the
1276
   * string is padded. Alias for pad() with a $padType of 'left'.
1277
   *
1278
   * @param  int    $length Desired string length after padding
1279
   * @param  string $padStr String used to pad, defaults to space
1280
   *
1281
   * @return static  String with left padding
1282 13
   */
1283
  public function padLeft($length, $padStr = ' ')
1284 13
  {
1285
    return $this->applyPadding($length - $this->length(), 0, $padStr);
1286
  }
1287
1288
  /**
1289
   * Adds the specified amount of left and right padding to the given string.
1290
   * The default character used is a space.
1291
   *
1292
   * @param  int    $left   Length of left padding
1293
   * @param  int    $right  Length of right padding
1294
   * @param  string $padStr String used to pad
1295
   *
1296 14
   * @return static  String with padding applied
1297
   */
1298 14
  protected function applyPadding($left = 0, $right = 0, $padStr = ' ')
1299
  {
1300 14
    $stringy = static::create($this->str, $this->encoding);
1301
1302
    $length = UTF8::strlen($padStr, $stringy->encoding);
1303
1304
    $strLength = $stringy->length();
1305
    $paddedLength = $strLength + $left + $right;
1306
1307
    if (!$length || $paddedLength <= $strLength) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $length of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1308
      return $stringy;
1309
    }
1310 2
1311
    $leftPadding = UTF8::substr(
1312 2
        UTF8::str_repeat(
1313
            $padStr,
1314
            ceil($left / $length)
1315
        ),
1316
        0,
1317
        $left,
1318
        $stringy->encoding
1319
    );
1320
1321
    $rightPadding = UTF8::substr(
1322 12
        UTF8::str_repeat(
1323
            $padStr,
1324 12
            ceil($right / $length)
1325
        ),
1326 12
        0,
1327 6
        $right,
1328
        $stringy->encoding
1329 6
    );
1330
1331
    $stringy->str = $leftPadding . $stringy->str . $rightPadding;
1332 6
1333
    return $stringy;
1334
  }
1335
1336
  /**
1337
   * Returns a new string of a given length such that the end of the string
1338
   * is padded. Alias for pad() with a $padType of 'right'.
1339
   *
1340
   * @param  int    $length Desired string length after padding
1341
   * @param  string $padStr String used to pad, defaults to space
1342 12
   *
1343
   * @return static  String with right padding
1344 12
   */
1345
  public function padRight($length, $padStr = ' ')
1346 12
  {
1347 8
    return $this->applyPadding(0, $length - $this->length(), $padStr);
1348
  }
1349 8
1350
  /**
1351
   * Returns a new string of a given length such that both sides of the
1352 4
   * string are padded. Alias for pad() with a $padType of 'both'.
1353
   *
1354
   * @param  int    $length Desired string length after padding
1355
   * @param  string $padStr String used to pad, defaults to space
1356
   *
1357
   * @return static  String with padding applied
1358
   */
1359
  public function padBoth($length, $padStr = ' ')
1360
  {
1361
    $padding = $length - $this->length();
1362 7
1363
    return $this->applyPadding(floor($padding / 2), ceil($padding / 2), $padStr);
1364 7
  }
1365
1366 7
  /**
1367
   * Returns a new string starting with $string.
1368
   *
1369
   * @param  string $string The string to append
1370
   *
1371
   * @return static  Object with appended $string
1372
   */
1373
  public function prepend($string)
1374
  {
1375
    return static::create($string . $this->str, $this->encoding);
1376
  }
1377
1378 28
  /**
1379
   * Returns a new string with the prefix $substring removed, if present.
1380 28
   *
1381 21
   * @param  string $substring The prefix to remove
1382 21
   *
1383 7
   * @return static  Object having a $str without the prefix $substring
1384
   */
1385 View Code Duplication
  public function removeLeft($substring)
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...
1386 28
  {
1387
    $stringy = static::create($this->str, $this->encoding);
1388
1389
    if ($stringy->startsWith($substring)) {
1390
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1391
1392
      return $stringy->substr($substringLength);
1393
    }
1394
1395
    return $stringy;
1396
  }
1397
1398 30
  /**
1399
   * Returns a new string with the suffix $substring removed, if present.
1400 30
   *
1401 23
   * @param  string $substring The suffix to remove
1402 23
   *
1403 7
   * @return static  Object having a $str without the suffix $substring
1404
   */
1405 View Code Duplication
  public function removeRight($substring)
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...
1406 30
  {
1407
    $stringy = static::create($this->str, $this->encoding);
1408
1409
    if ($stringy->endsWith($substring)) {
1410
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1411
1412
      return $stringy->substr(0, $stringy->length() - $substringLength);
1413
    }
1414
1415
    return $stringy;
1416
  }
1417 16
1418
  /**
1419 16
   * Returns a repeated string given a multiplier.
1420
   *
1421 16
   * @param  int $multiplier The number of times to repeat the string
1422
   *
1423
   * @return static  Object with a repeated str
1424
   */
1425
  public function repeat($multiplier)
1426
  {
1427
    $repeated = UTF8::str_repeat($this->str, $multiplier);
1428
1429
    return static::create($repeated, $this->encoding);
1430
  }
1431
1432 16
  /**
1433
   * Replaces all occurrences of $search in $str by $replacement.
1434 16
   *
1435
   * @param string $search        The needle to search for
1436 16
   * @param string $replacement   The string to replace with
1437
   * @param bool   $caseSensitive Whether or not to enforce case-sensitivity
1438
   *
1439
   * @return static  Object with the resulting $str after the replacements
1440
   */
1441 View Code Duplication
  public function replace($search, $replacement, $caseSensitive = true)
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...
1442
  {
1443
    if ($caseSensitive) {
1444 5
      $return = UTF8::str_replace($search, $replacement, $this->str);
1445
    } else {
1446 5
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1447
    }
1448 5
1449
    return static::create($return);
1450
  }
1451
1452
  /**
1453
   * Replaces all occurrences of $search in $str by $replacement.
1454
   *
1455
   * @param array        $search        The elements to search for
1456
   * @param string|array $replacement   The string to replace with
1457
   * @param bool         $caseSensitive Whether or not to enforce case-sensitivity
1458
   *
1459
   * @return static  Object with the resulting $str after the replacements
1460
   */
1461 View Code Duplication
  public function replaceAll(array $search, $replacement, $caseSensitive = true)
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...
1462 23
  {
1463
    if ($caseSensitive) {
1464 23
      $return = UTF8::str_replace($search, $replacement, $this->str);
1465 23
    } else {
1466 4
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1467
    }
1468
1469
    return static::create($return);
1470 19
  }
1471 19
1472 19
  /**
1473
   * Replaces all occurrences of $search from the beginning of string with $replacement
1474 19
   *
1475
   * @param string $search
1476
   * @param string $replacement
1477 19
   *
1478 19
   * @return static  Object with the resulting $str after the replacements
1479
   */
1480 12
  public function replaceBeginning($search, $replacement)
1481
  {
1482 12
    $str = $this->regexReplace('^' . preg_quote($search, '/'), UTF8::str_replace('\\', '\\\\', $replacement));
1483 11
1484 11
    return static::create($str, $this->encoding);
1485 12
  }
1486
1487 19
  /**
1488
   * Replaces all occurrences of $search from the ending of string with $replacement
1489 19
   *
1490
   * @param string $search
1491
   * @param string $replacement
1492
   *
1493
   * @return static  Object with the resulting $str after the replacements
1494
   */
1495
  public function replaceEnding($search, $replacement)
1496
  {
1497
    $str = $this->regexReplace(preg_quote($search, '/') . '$', UTF8::str_replace('\\', '\\\\', $replacement));
1498 3
1499
    return static::create($str, $this->encoding);
1500 3
  }
1501
1502 3
  /**
1503
   * Gets the substring after (or before via "$beforeNeedle") the first occurrence of the "$needle".
1504
   * If no match is found returns new empty Stringy object.
1505
   *
1506
   * @param string $needle
1507
   * @param bool   $beforeNeedle
1508
   *
1509
   * @return static
1510
   */
1511 View Code Duplication
  public function substringOf($needle, $beforeNeedle = false)
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...
1512
  {
1513
    if ('' === $needle) {
1514
      return static::create('');
1515
    }
1516
1517
    if (false === $part = UTF8::strstr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1518 15
      return static::create('');
1519
    }
1520 15
1521
    return static::create($part);
1522 15
  }
1523
1524
  /**
1525
   * Gets the substring after (or before via "$beforeNeedle") the first occurrence of the "$needle".
1526
   * If no match is found returns new empty Stringy object.
1527
   *
1528
   * @param string $needle
1529
   * @param bool   $beforeNeedle
1530 1
   *
1531
   * @return static
1532 1
   */
1533 View Code Duplication
  public function substringOfIgnoreCase($needle, $beforeNeedle = false)
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...
1534 1
  {
1535
    if ('' === $needle) {
1536
      return static::create('');
1537
    }
1538
1539
    if (false === $part = UTF8::stristr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1540
      return static::create('');
1541
    }
1542
1543
    return static::create($part);
1544 1
  }
1545
1546 1
  /**
1547
   * Gets the substring after (or before via "$beforeNeedle") the last occurrence of the "$needle".
1548 1
   * If no match is found returns new empty Stringy object.
1549
   *
1550
   * @param string $needle
1551
   * @param bool   $beforeNeedle
1552
   *
1553
   * @return static
1554
   */
1555 View Code Duplication
  public function lastSubstringOf($needle, $beforeNeedle = false)
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...
1556 1
  {
1557
    if ('' === $needle) {
1558 1
      return static::create('');
1559
    }
1560
1561
    if (false === $part = UTF8::strrchr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1562
      return static::create('');
1563
    }
1564
1565
    return static::create($part);
1566 6
  }
1567
1568 6
  /**
1569 6
   * Gets the substring after (or before via "$beforeNeedle") the last occurrence of the "$needle".
1570 6
   * If no match is found returns new empty Stringy object.
1571 6
   *
1572 6
   * @param string $needle
1573
   * @param bool   $beforeNeedle
1574 6
   *
1575
   * @return static
1576
   */
1577 View Code Duplication
  public function lastSubstringOfIgnoreCase($needle, $beforeNeedle = false)
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...
1578
  {
1579
    if ('' === $needle) {
1580
      return static::create('');
1581
    }
1582
1583
    if (false === $part = UTF8::strrichr($this->str, $needle, $beforeNeedle, $this->encoding)) {
1584
      return static::create('');
1585
    }
1586 1
1587
    return static::create($part);
1588
  }
1589 1
1590
  /**
1591 1
   * Returns a reversed string. A multibyte version of strrev().
1592 1
   *
1593
   * @return static  Object with a reversed $str
1594
   */
1595 1
  public function reverse()
1596
  {
1597 1
    $reversed = UTF8::strrev($this->str);
1598 1
1599 1
    return static::create($reversed, $this->encoding);
1600
  }
1601 1
1602
  /**
1603
   * Truncates the string to a given length, while ensuring that it does not
1604
   * split words. If $substring is provided, and truncating occurs, the
1605
   * string is further truncated so that the substring may be appended without
1606
   * exceeding the desired length.
1607
   *
1608
   * @param  int    $length    Desired length of the truncated string
1609
   * @param  string $substring The substring to append if it can fit
1610
   *
1611
   * @return static  Object with the resulting $str after truncating
1612
   */
1613
  public function safeTruncate($length, $substring = '')
1614
  {
1615
    $stringy = static::create($this->str, $this->encoding);
1616
    if ($length >= $stringy->length()) {
1617
      return $stringy;
1618
    }
1619
1620
    // Need to further trim the string so we can append the substring
1621 1
    $encoding = $stringy->encoding;
1622 1
    $substringLength = UTF8::strlen($substring, $encoding);
1623 1
    $length -= $substringLength;
1624 1
1625 1
    $truncated = UTF8::substr($stringy->str, 0, $length, $encoding);
1626 1
1627 1
    // If the last word was truncated
1628
    $strPosSpace = UTF8::strpos($stringy->str, ' ', $length - 1, $encoding);
1629 1
    if ($strPosSpace != $length) {
1630
      // Find pos of the last occurrence of a space, get up to that
1631 1
      $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 1625 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...
1632 1
1633
      if ($lastPos !== false || $strPosSpace !== false) {
1634 1
        $truncated = UTF8::substr($truncated, 0, $lastPos, $encoding);
0 ignored issues
show
Security Bug introduced by
It seems like $truncated defined by \voku\helper\UTF8::subst...0, $lastPos, $encoding) on line 1634 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...
Bug introduced by
It seems like $lastPos defined by \voku\helper\UTF8::strrp...ted, ' ', 0, $encoding) on line 1631 can also be of type double or false; however, voku\helper\UTF8::substr() does only seem to accept integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1635 1
      }
1636 1
    }
1637
1638 1
    $stringy->str = $truncated . $substring;
1639 1
1640
    return $stringy;
1641
  }
1642 1
1643 1
  /**
1644 1
   * A multibyte string shuffle function. It returns a string with its
1645
   * characters in random order.
1646 1
   *
1647
   * @return static  Object with a shuffled $str
1648
   */
1649
  public function shuffle()
1650 1
  {
1651 1
    $shuffledStr = UTF8::str_shuffle($this->str);
1652 1
1653 1
    return static::create($shuffledStr, $this->encoding);
1654
  }
1655 1
1656 1
  /**
1657 1
   * Converts the string into an URL slug. This includes replacing non-ASCII
1658 1
   * characters with their closest ASCII equivalents, removing remaining
1659 1
   * non-ASCII and non-alphanumeric characters, and replacing whitespace with
1660 1
   * $replacement. The replacement defaults to a single dash, and the string
1661 1
   * is also converted to lowercase.
1662 1
   *
1663
   * @param string $replacement The string used to replace whitespace
1664 1
   * @param string $language    The language for the url
1665 1
   * @param bool   $strToLower  string to lower
1666 1
   *
1667 1
   * @return static  Object whose $str has been converted to an URL slug
1668 1
   */
1669 1
  public function slugify($replacement = '-', $language = 'de', $strToLower = true)
1670 1
  {
1671 1
    $slug = URLify::slug($this->str, $language, $replacement, $strToLower);
1672 1
1673
    return static::create($slug, $this->encoding);
1674 1
  }
1675
1676
  /**
1677 1
   * Remove css media-queries.
1678
   *
1679 1
   * @return static
1680 1
   */
1681
  public function stripeCssMediaQueries()
1682 1
  {
1683
    $pattern = '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU';
1684
1685
    return static::create(preg_replace($pattern, '', $this->str));
1686 1
  }
1687 1
1688 1
  /**
1689 1
   * Strip all whitespace characters. This includes tabs and newline characters,
1690
   * as well as multibyte whitespace such as the thin space and ideographic space.
1691 1
   *
1692 1
   * @return Stringy
1693 1
   */
1694
  public function stripWhitespace()
1695 1
  {
1696 1
    return static::create(UTF8::strip_whitespace($this->str));
1697 1
  }
1698
1699
  /**
1700
   * Remove empty html-tag.
1701 1
   *
1702
   * e.g.: <tag></tag>
1703
   *
1704
   * @return static
1705
   */
1706
  public function stripeEmptyHtmlTags()
1707
  {
1708
    $pattern = "/<[^\/>]*>(([\s]?)*|)<\/[^>]*>/i";
1709
1710 6
    return static::create(preg_replace($pattern, '', $this->str));
1711
  }
1712 6
1713
  /**
1714 6
   * Converts the string into an valid UTF-8 string.
1715 1
   *
1716 1
   * @return static
1717
   */
1718 6
  public function utf8ify()
1719
  {
1720 6
    return static::create(UTF8::cleanup($this->str));
1721
  }
1722
1723
  /**
1724
   * escape html
1725
   *
1726
   * @return static
1727
   */
1728
  public function escape()
1729
  {
1730 6
    $str = UTF8::htmlspecialchars(
1731
        $this->str,
1732 6
        ENT_QUOTES | ENT_SUBSTITUTE,
1733
        $this->encoding
1734 6
    );
1735
1736
    return static::create($str, $this->encoding);
1737
  }
1738
1739
  /**
1740
   * Create an extract from a text, so if the search-string was found, it will be centered in the output.
1741
   *
1742
   * @param string   $search
1743
   * @param int|null $length
1744 6
   * @param string   $ellipsis
1745
   *
1746 6
   * @return static
1747
   */
1748 6
  public function extractText($search = '', $length = null, $ellipsis = '...')
1749
  {
1750
    // init
1751
    $text = $this->str;
1752
1753
    if (empty($text)) {
1754
      return static::create('', $this->encoding);
1755
    }
1756
1757
    $trimChars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1758
1759
    if ($length === null) {
1760
      $length = (int)round($this->length() / 2, 0);
1761
    }
1762 18
1763
    if (empty($search)) {
1764 18
1765 4
      $stringLength = UTF8::strlen($text, $this->encoding);
1766 18
      $end = ($length - 1) > $stringLength ? $stringLength : ($length - 1);
1767 4
      $pos = min(UTF8::strpos($text, ' ', $end, 0, $this->encoding), UTF8::strpos($text, '.', $end));
0 ignored issues
show
Documentation introduced by
$this->encoding is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1768 10
1769 2
      if ($pos) {
1770 2
        return static::create(
1771 8
            rtrim(
1772
                UTF8::substr($text, 0, $pos, $this->encoding),
1773
                $trimChars
1774 14
            ) . $ellipsis,
1775
            $this->encoding
1776 14
        );
1777
      }
1778
1779
      return static::create($text, $this->encoding);
1780
    }
1781
1782
    $wordPos = UTF8::strpos(
1783
        UTF8::strtolower($text),
1784
        UTF8::strtolower($search, $this->encoding),
1785
        null,
1786
        $this->encoding
1787
    );
1788
    $halfSide = (int)($wordPos - $length / 2 + UTF8::strlen($search, $this->encoding) / 2);
1789 19
1790
    if ($halfSide > 0) {
1791 19
1792 2
      $halfText = UTF8::substr($text, 0, $halfSide, $this->encoding);
1793
      $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 1792 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...
1794
1795
      if (!$pos_start) {
1796
        $pos_start = 0;
1797 17
      }
1798 1
1799
    } else {
1800
      $pos_start = 0;
1801
    }
1802
1803 16
    if ($wordPos && $halfSide > 0) {
1804 8
      $l = $pos_start + $length - 1;
1805 8
      $realLength = UTF8::strlen($text, $this->encoding);
1806 8
1807
      if ($l > $realLength) {
1808
        $l = $realLength;
1809 16
      }
1810
1811 16
      $pos_end = min(
1812 4
                     UTF8::strpos($text, ' ', $l, $this->encoding),
1813 4
                     UTF8::strpos($text, '.', $l, $this->encoding)
1814
                 ) - $pos_start;
1815
1816
      if (!$pos_end || $pos_end <= 0) {
1817 16
        $extract = $ellipsis . ltrim(
1818 16
                UTF8::substr(
1819 16
                    $text,
1820
                    $pos_start,
1821 16
                    UTF8::strlen($text),
1822
                    $this->encoding
1823
                ),
1824
                $trimChars
1825
            );
1826 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...
1827
        $extract = $ellipsis . trim(
1828
                UTF8::substr(
1829
                    $text,
1830
                    $pos_start,
1831
                    $pos_end,
1832 5
                    $this->encoding
1833
                ),
1834 5
                $trimChars
1835
            ) . $ellipsis;
1836 5
      }
1837
1838
    } else {
1839
1840
      $l = $length - 1;
1841
      $trueLength = UTF8::strlen($text, $this->encoding);
1842
1843
      if ($l > $trueLength) {
1844 5
        $l = $trueLength;
1845
      }
1846 5
1847
      $pos_end = min(
1848 5
          UTF8::strpos($text, ' ', $l, $this->encoding),
1849
          UTF8::strpos($text, '.', $l, $this->encoding)
1850 5
      );
1851
1852 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...
1853
        $extract = rtrim(
1854
                       UTF8::substr($text, 0, $pos_end, $this->encoding),
1855
                       $trimChars
1856
                   ) . $ellipsis;
1857
      } else {
1858
        $extract = $text;
1859
      }
1860 4
    }
1861
1862 4
    return static::create($extract, $this->encoding);
1863
  }
1864 4
1865
1866
  /**
1867
   * remove xss from html
1868
   *
1869
   * @return static
1870
   */
1871
  public function removeXss()
1872
  {
1873
    static $antiXss = null;
1874
1875
    if ($antiXss === null) {
1876 5
      $antiXss = new AntiXSS();
1877
    }
1878 5
1879 5
    $str = $antiXss->xss_clean($this->str);
1880
1881 5
    return static::create($str, $this->encoding);
1882 5
  }
1883
1884 5
  /**
1885 2
   * remove html-break [br | \r\n | \r | \n | ...]
1886
   *
1887 5
   * @param string $replacement
1888
   *
1889 5
   * @return static
1890
   */
1891 5
  public function removeHtmlBreak($replacement = '')
1892 5
  {
1893 5
    $str = preg_replace('#/\r\n|\r|\n|<br.*/?>#isU', $replacement, $this->str);
1894
1895 5
    return static::create($str, $this->encoding);
1896
  }
1897
1898
  /**
1899
   * remove html
1900
   *
1901
   * @param $allowableTags
1902
   *
1903
   * @return static
1904 27
   */
1905
  public function removeHtml($allowableTags = null)
1906 27
  {
1907
    $str = strip_tags($this->str, $allowableTags);
1908 27
1909
    return static::create($str, $this->encoding);
1910
  }
1911
1912
  /**
1913
   * Returns the substring beginning at $start, and up to, but not including
1914
   * the index specified by $end. If $end is omitted, the function extracts
1915
   * the remaining string. If $end is negative, it is computed from the end
1916 7
   * of the string.
1917
   *
1918 7
   * @param  int $start Initial index from which to begin extraction
1919
   * @param  int $end   Optional index at which to end extraction
1920
   *
1921
   * @return static  Object with its $str being the extracted substring
1922
   */
1923
  public function slice($start, $end = null)
1924
  {
1925
    if ($end === null) {
1926
      $length = $this->length();
1927
    } elseif ($end >= 0 && $end <= $start) {
1928
      return static::create('', $this->encoding);
1929
    } elseif ($end < 0) {
1930 16
      $length = $this->length() + $end - $start;
1931
    } else {
1932 16
      $length = $end - $start;
1933
    }
1934 16
1935
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
1936
1937
    return static::create($str, $this->encoding);
1938
  }
1939
1940
  /**
1941
   * Splits the string with the provided regular expression, returning an
1942
   * array of Stringy objects. An optional integer $limit will truncate the
1943
   * results.
1944
   *
1945
   * @param  string $pattern The regex with which to split the string
1946
   * @param  int    $limit   Optional maximum number of results to return
1947
   *
1948 15
   * @return static [] An array of Stringy objects
1949
   */
1950 15
  public function split($pattern, $limit = null)
1951
  {
1952 15
    if ($limit === 0) {
1953 15
      return array();
1954 15
    }
1955 15
1956 15
    // this->split errors when supplied an empty pattern in < PHP 5.4.13
1957 15
    // and current versions of HHVM (3.8 and below)
1958 15
    if ($pattern === '') {
1959 15
      return array(static::create($this->str, $this->encoding));
1960 15
    }
1961
1962 15
    // this->split returns the remaining unsplit string in the last index when
1963 10
    // supplying a limit
1964 5
    if ($limit > 0) {
1965 2
      $limit += 1;
1966
    } else {
1967 3
      $limit = -1;
1968
    }
1969
1970
    $array = preg_split('/' . preg_quote($pattern, '/') . '/u', $this->str, $limit);
1971
1972
    if ($limit > 0 && count($array) === $limit) {
1973
      array_pop($array);
1974
    }
1975
1976 993
    /** @noinspection CallableInLoopTerminationConditionInspection */
1977
    /** @noinspection ForeachInvariantsInspection */
1978 993 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...
1979
      $array[$i] = static::create($array[$i], $this->encoding);
1980
    }
1981
1982
    return $array;
1983
  }
1984
1985
  /**
1986
   * Surrounds $str with the given substring.
1987
   *
1988
   * @param  string $substring The substring to add to both sides
1989 6
   *
1990
   * @return static  Object whose $str had the substring both prepended and
1991 6
   *                 appended
1992 6
   */
1993
  public function surround($substring)
1994 6
  {
1995
    $str = implode('', array($substring, $this->str, $substring));
1996
1997
    return static::create($str, $this->encoding);
1998
  }
1999
2000
  /**
2001
   * Returns a case swapped version of the string.
2002
   *
2003
   * @return static  Object whose $str has each character's case swapped
2004
   */
2005
  public function swapCase()
2006 5
  {
2007
    $stringy = static::create($this->str, $this->encoding);
2008 5
2009 5
    $stringy->str = UTF8::swapCase($stringy->str, $stringy->encoding);
2010
2011 5
    return $stringy;
2012
  }
2013
2014
  /**
2015
   * Returns a string with smart quotes, ellipsis characters, and dashes from
2016
   * Windows-1252 (commonly used in Word documents) replaced by their ASCII
2017
   * equivalents.
2018
   *
2019 5
   * @return static  Object whose $str has those characters removed
2020
   */
2021
  public function tidy()
2022 5
  {
2023
    $str = UTF8::normalize_msword($this->str);
2024 5
2025
    return static::create($str, $this->encoding);
2026
  }
2027
2028
  /**
2029
   * Returns a trimmed string with the first letter of each word capitalized.
2030
   * Also accepts an array, $ignore, allowing you to list words not to be
2031
   * capitalized.
2032
   *
2033 5
   * @param  array $ignore An array of words not to capitalize
2034
   *
2035 5
   * @return static  Object with a titleized $str
2036
   */
2037 5
  public function titleize($ignore = null)
2038
  {
2039
    $stringy = static::create($this->trim(), $this->encoding);
2040
    $encoding = $this->encoding;
2041
2042
    $stringy->str = preg_replace_callback(
2043
        '/([\S]+)/u',
2044
        function ($match) use ($encoding, $ignore) {
2045
          if ($ignore && in_array($match[0], $ignore, true)) {
2046
            return $match[0];
2047
          }
2048
2049 13
          $stringy = new Stringy($match[0], $encoding);
2050
2051 13
          return (string)$stringy->toLowerCase()->upperCaseFirst();
2052 11
        },
2053 11
        $stringy->str
2054 2
    );
2055
2056
    return $stringy;
2057 13
  }
2058
2059
  /**
2060
   * Converts all characters in the string to lowercase. An alias for PHP's
2061
   * UTF8::strtolower().
2062
   *
2063
   * @return static  Object with all characters of $str being lowercase
2064
   */
2065
  public function toLowerCase()
2066
  {
2067
    $str = UTF8::strtolower($this->str, $this->encoding);
2068
2069 13
    return static::create($str, $this->encoding);
2070
  }
2071 13
2072 11
  /**
2073 11
   * Returns true if the string is base64 encoded, false otherwise.
2074 2
   *
2075
   * @return bool Whether or not $str is base64 encoded
2076
   */
2077 13
  public function isBase64()
2078
  {
2079
    return UTF8::is_base64($this->str);
2080
  }
2081
2082
  /**
2083
   * Returns an ASCII version of the string. A set of non-ASCII characters are
2084
   * replaced with their closest ASCII counterparts, and the rest are removed
2085
   * unless instructed otherwise.
2086
   *
2087
   * @param $strict [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad performance</p>
2088
   *
2089
   * @return static  Object whose $str contains only ASCII characters
2090 22
   */
2091
  public function toAscii($strict = false)
2092 22
  {
2093 22
    $str = UTF8::to_ascii($this->str, '?', $strict);
2094 4
2095
    return static::create($str, $this->encoding);
2096
  }
2097
2098 18
  /**
2099 18
   * Returns a boolean representation of the given logical string value.
2100
   * For example, 'true', '1', 'on' and 'yes' will return true. 'false', '0',
2101 18
   * 'off', and 'no' will return false. In all instances, case is ignored.
2102 18
   * For other numeric strings, their sign will determine the return value.
2103
   * In addition, blank strings consisting of only whitespace will return
2104 18
   * false. For all other strings, the return value is a result of a
2105
   * boolean cast.
2106
   *
2107
   * @return bool A boolean value for the string
2108
   */
2109
  public function toBoolean()
2110
  {
2111
    $key = $this->toLowerCase()->str;
2112
    $map = array(
2113
        'true'  => true,
2114
        '1'     => true,
2115 16
        'on'    => true,
2116
        'yes'   => true,
2117 16
        'false' => false,
2118
        '0'     => false,
2119
        'off'   => false,
2120
        'no'    => false,
2121
    );
2122
2123
    if (array_key_exists($key, $map)) {
2124
      return $map[$key];
2125
    }
2126
2127 13
    if (is_numeric($this->str)) {
2128
      return ((int)$this->str > 0);
2129 13
    }
2130
2131
    return (bool)$this->regexReplace('[[:space:]]', '')->str;
2132
  }
2133
2134
  /**
2135
   * Return Stringy object as string, but you can also use (string) for automatically casting the object into a string.
2136
   *
2137
   * @return string
2138
   */
2139 32
  public function toString()
2140
  {
2141 32
    return (string)$this->str;
2142 32
  }
2143 32
2144
  /**
2145 32
   * Converts each tab in the string to some number of spaces, as defined by
2146 32
   * $tabLength. By default, each tab is converted to 4 consecutive spaces.
2147
   *
2148 27
   * @param  int $tabLength Number of spaces to replace each tab with
2149 27
   *
2150
   * @return static  Object whose $str has had tabs switched to spaces
2151 1
   */
2152 View Code Duplication
  public function toSpaces($tabLength = 4)
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...
2153 32
  {
2154 32
    $spaces = UTF8::str_repeat(' ', $tabLength);
2155 32
    $str = UTF8::str_replace("\t", $spaces, $this->str);
2156
2157 32
    return static::create($str, $this->encoding);
2158 32
  }
2159
2160 6
  /**
2161 32
   * Converts each occurrence of some consecutive number of spaces, as
2162 32
   * defined by $tabLength, to a tab. By default, each 4 consecutive spaces
2163 32
   * are converted to a tab.
2164
   *
2165 32
   * @param  int $tabLength Number of spaces to replace with a tab
2166
   *
2167
   * @return static  Object whose $str has had spaces switched to tabs
2168
   */
2169 View Code Duplication
  public function toTabs($tabLength = 4)
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...
2170
  {
2171
    $spaces = UTF8::str_repeat(' ', $tabLength);
2172
    $str = UTF8::str_replace($spaces, "\t", $this->str);
2173 20
2174
    return static::create($str, $this->encoding);
2175 20
  }
2176
2177 20
  /**
2178 20
   * Converts the first character of each word in the string to uppercase.
2179 20
   *
2180
   * @return static  Object with all characters of $str being title-cased
2181 20
   */
2182 20
  public function toTitleCase()
2183 20
  {
2184 8
    // "mb_convert_case()" used a polyfill from the "UTF8"-Class
2185 8
    $str = mb_convert_case($this->str, MB_CASE_TITLE, $this->encoding);
2186
2187 8
    return static::create($str, $this->encoding);
2188 4
  }
2189
2190 4
  /**
2191
   * Converts all characters in the string to uppercase. An alias for PHP's
2192 20
   * UTF8::strtoupper().
2193
   *
2194 20
   * @return static  Object with all characters of $str being uppercase
2195
   */
2196 20
  public function toUpperCase()
2197
  {
2198
    $str = UTF8::strtoupper($this->str, $this->encoding);
2199 20
2200 20
    return static::create($str, $this->encoding);
2201 20
  }
2202 20
2203
  /**
2204 20
   * Returns a string with whitespace removed from the start of the string.
2205 20
   * Supports the removal of unicode whitespace. Accepts an optional
2206 20
   * string of characters to strip instead of the defaults.
2207 20
   *
2208
   * @param  string $chars Optional string of characters to strip
2209 20
   *
2210
   * @return static  Object with a trimmed $str
2211 20
   */
2212 20 View Code Duplication
  public function trimLeft($chars = null)
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...
2213
  {
2214 20
    if (!$chars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chars of type string|null 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...
2215
      $chars = '[:space:]';
2216
    } else {
2217
      $chars = preg_quote($chars, '/');
2218
    }
2219
2220
    return $this->regexReplace("^[$chars]+", '');
2221
  }
2222 37
2223
  /**
2224 37
   * Returns a string with whitespace removed from the end of the string.
2225 37
   * Supports the removal of unicode whitespace. Accepts an optional
2226
   * string of characters to strip instead of the defaults.
2227 37
   *
2228
   * @param  string $chars Optional string of characters to strip
2229 37
   *
2230
   * @return static  Object with a trimmed $str
2231
   */
2232 View Code Duplication
  public function trimRight($chars = null)
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...
2233
  {
2234
    if (!$chars) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chars of type string|null 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...
2235
      $chars = '[:space:]';
2236
    } else {
2237
      $chars = preg_quote($chars, '/');
2238
    }
2239
2240 4
    return $this->regexReplace("[$chars]+\$", '');
2241
  }
2242 4
2243
  /**
2244 4
   * Truncates the string to a given length. If $substring is provided, and
2245
   * truncating occurs, the string is further truncated so that the substring
2246
   * may be appended without exceeding the desired length.
2247
   *
2248
   * @param  int    $length    Desired length of the truncated string
2249
   * @param  string $substring The substring to append if it can fit
2250
   *
2251
   * @return static  Object with the resulting $str after truncating
2252
   */
2253 View Code Duplication
  public function truncate($length, $substring = '')
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...
2254 1
  {
2255
    $stringy = static::create($this->str, $this->encoding);
2256 1
    if ($length >= $stringy->length()) {
2257
      return $stringy;
2258 1
    }
2259 1
2260 1
    // Need to further trim the string so we can append the substring
2261 1
    $substringLength = UTF8::strlen($substring, $stringy->encoding);
2262 1
    $length -= $substringLength;
2263
2264 1
    $truncated = UTF8::substr($stringy->str, 0, $length, $stringy->encoding);
2265
    $stringy->str = $truncated . $substring;
2266
2267
    return $stringy;
2268
  }
2269
2270
  /**
2271
   * Returns a lowercase and trimmed string separated by underscores.
2272
   * Underscores are inserted before uppercase characters (with the exception
2273
   * of the first character of the string), and in place of spaces as well as
2274
   * dashes.
2275 1
   *
2276
   * @return static  Object with an underscored $str
2277 1
   */
2278 1
  public function underscored()
2279
  {
2280
    return $this->delimit('_');
2281 1
  }
2282 1
2283 1
  /**
2284 1
   * Returns an UpperCamelCase version of the supplied string. It trims
2285 1
   * surrounding spaces, capitalizes letters following digits, spaces, dashes
2286 1
   * and underscores, and removes spaces, dashes, underscores.
2287 1
   *
2288 1
   * @return static  Object with $str in UpperCamelCase
2289 1
   */
2290
  public function upperCamelize()
2291
  {
2292
    return $this->camelize()->upperCaseFirst();
2293
  }
2294
2295
  /**
2296
   * Returns a camelCase version of the string. Trims surrounding spaces,
2297
   * capitalizes letters following digits, spaces, dashes and underscores,
2298
   * and removes spaces, dashes, as well as underscores.
2299
   *
2300 1
   * @return static  Object with $str in camelCase
2301
   */
2302 1
  public function camelize()
2303 1
  {
2304 1
    $encoding = $this->encoding;
2305
    $stringy = $this->trim()->lowerCaseFirst();
2306
    $stringy->str = preg_replace('/^[-_]+/', '', $stringy->str);
2307 1
2308 1
    $stringy->str = preg_replace_callback(
2309 1
        '/[-_\s]+(.)?/u',
2310 1
        function ($match) use ($encoding) {
2311 1
          if (isset($match[1])) {
2312 1
            return UTF8::strtoupper($match[1], $encoding);
2313 1
          }
2314 1
2315 1
          return '';
2316
        },
2317
        $stringy->str
2318
    );
2319
2320
    $stringy->str = preg_replace_callback(
2321
        '/[\d]+(.)?/u',
2322
        function ($match) use ($encoding) {
2323
          return UTF8::strtoupper($match[0], $encoding);
2324
        },
2325
        $stringy->str
2326 1
    );
2327
2328 1
    return $stringy;
2329 1
  }
2330 1
2331
  /**
2332
   * Convert a string to e.g.: "snake_case"
2333 1
   *
2334 1
   * @return static  Object with $str in snake_case
2335 1
   */
2336 1
  public function snakeize()
2337 1
  {
2338 1
    $str = $this->str;
2339 1
2340 1
    $encoding = $this->encoding;
2341 1
    $str = UTF8::normalize_whitespace($str);
2342
    $str = str_replace('-', '_', $str);
2343
2344
    $str = preg_replace_callback(
2345
        '/([\d|A-Z])/u',
2346
        function ($matches) use ($encoding) {
2347
          $match = $matches[1];
2348
          $matchInt = (int)$match;
2349
2350
          if ("$matchInt" == $match) {
2351
            return '_' . $match . '_';
2352 1
          }
2353
2354 1
          return '_' . UTF8::strtolower($match, $encoding);
2355 1
        },
2356 1
        $str
2357
    );
2358
2359 1
    $str = preg_replace(
2360 1
        array(
2361 1
2362 1
            '/\s+/',      // convert spaces to "_"
2363 1
            '/^\s+|\s+$/',  // trim leading & trailing spaces
2364 1
            '/_+/',         // remove double "_"
2365 1
        ),
2366 1
        array(
2367 1
            '_',
2368
            '',
2369
            '_',
2370
        ),
2371
        $str
2372
    );
2373
2374
    $str = UTF8::trim($str, '_'); // trim leading & trailing "_"
2375
    $str = UTF8::trim($str); // trim leading & trailing whitespace
2376 39
2377
    return static::create($str, $this->encoding);
2378 39
  }
2379 39
2380 39
  /**
2381
   * Converts the first character of the string to lower case.
2382 39
   *
2383
   * @return static  Object with the first character of $str being lower case
2384
   */
2385 View Code Duplication
  public function lowerCaseFirst()
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
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
2388
    $rest = UTF8::substr($this->str, 1, $this->length() - 1, $this->encoding);
2389
2390 7
    $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 2387 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...
2391
2392 7
    return static::create($str, $this->encoding);
2393
  }
2394 7
2395 7
  /**
2396 7
   * Shorten the string after $length, but also after the next word.
2397
   *
2398 7
   * @param int    $length
2399
   * @param string $strAddOn
2400
   *
2401
   * @return static
2402
   */
2403
  public function shortenAfterWord($length, $strAddOn = '...')
2404
  {
2405
    $string = UTF8::str_limit_after_word($this->str, $length, $strAddOn);
2406
2407
    return static::create($string);
2408
  }
2409 39
2410
  /**
2411
   * Line-Wrap the string after $limit, but also after the next word.
2412 39
   *
2413 39
   * @param int $limit
2414
   *
2415
   * @return static
2416
   */
2417 39
  public function lineWrapAfterWord($limit)
2418 39
  {
2419 39
    $strings = preg_split('/\\r\\n|\\r|\\n/', $this->str);
2420 39
2421 39
    $string = '';
2422 39
    foreach ($strings as $value) {
2423 39
      $string .= wordwrap($value, $limit);
2424 39
      $string .= "\n";
2425 39
    }
2426 39
2427 39
    return static::create($string);
2428 39
  }
2429 39
2430 39
  /**
2431 39
   * Gets the substring after the first occurrence of a separator.
2432 39
   * If no match is found returns new empty Stringy object.
2433 39
   *
2434 39
   * @param string $separator
2435 39
   *
2436 39
   * @return static
2437 39
   */
2438 39 View Code Duplication
  public function afterFirst($separator)
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...
2439 39
  {
2440 39
    if ($separator === '') {
2441 39
      return static::create('');
2442 39
    }
2443 39
2444
    if ($this->str === '') {
2445 39
      return static::create('');
2446 39
    }
2447 39
2448 39
    if (($offset = $this->indexOf($separator)) === false) {
2449 39
      return static::create('');
2450 39
    }
2451 39
2452 39
    return static::create(
2453 39
        UTF8::substr(
2454
            $this->str,
2455 39
            $offset + UTF8::strlen($separator, $this->encoding),
2456 39
            null,
2457 27
            $this->encoding
2458
        ),
2459
        $this->encoding
2460 13
    );
2461
  }
2462 13
2463 13
  /**
2464 13
   * Gets the substring after the first occurrence of a separator.
2465 2
   * If no match is found returns new empty Stringy object.
2466 2
   *
2467 13
   * @param string $separator
2468 13
   *
2469
   * @return static
2470 13
   */
2471 13 View Code Duplication
  public function afterFirstIgnoreCase($separator)
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...
2472 7
  {
2473 7
    if ($separator === '') {
2474 13
      return static::create('');
2475
    }
2476 13
2477 7
    if ($this->str === '') {
2478
      return static::create('');
2479
    }
2480 7
2481 39
    if (($offset = $this->indexOfIgnoreCase($separator)) === false) {
2482
      return static::create('');
2483 39
    }
2484
2485
    return static::create(
2486
        UTF8::substr(
2487
            $this->str,
2488
            $offset + UTF8::strlen($separator, $this->encoding),
2489
            null,
2490
            $this->encoding
2491
        ),
2492
        $this->encoding
2493
    );
2494
  }
2495
2496
  /**
2497
   * Gets the substring after the last occurrence of a separator.
2498
   * If no match is found returns new empty Stringy object.
2499
   *
2500
   * @param string $separator
2501
   *
2502
   * @return static
2503
   */
2504 View Code Duplication
  public function afterLastIgnoreCase($separator)
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...
2505
  {
2506
    if ($separator === '') {
2507
      return static::create('');
2508
    }
2509
2510
    if ($this->str === '') {
2511
      return static::create('');
2512
    }
2513
2514
    $offset = $this->indexOfLastIgnoreCase($separator);
2515
    if ($offset === false) {
2516
      return static::create('', $this->encoding);
2517
    }
2518
2519
    return static::create(
2520
        UTF8::substr(
2521
            $this->str,
2522
            $offset + UTF8::strlen($separator, $this->encoding),
2523
            null,
2524
            $this->encoding
2525
        ),
2526
        $this->encoding
2527
    );
2528
  }
2529
2530
  /**
2531
   * Gets the substring after the last occurrence of a separator.
2532
   * If no match is found returns new empty Stringy object.
2533
   *
2534
   * @param string $separator
2535
   *
2536
   * @return static
2537
   */
2538 View Code Duplication
  public function afterLast($separator)
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...
2539
  {
2540
    if ($separator === '') {
2541
      return static::create('');
2542
    }
2543
2544
    if ($this->str === '') {
2545
      return static::create('');
2546
    }
2547
2548
    $offset = $this->indexOfLast($separator);
2549
    if ($offset === false) {
2550
      return static::create('', $this->encoding);
2551
    }
2552
2553
    return static::create(
2554
        UTF8::substr(
2555
            $this->str,
2556
            $offset + UTF8::strlen($separator, $this->encoding),
2557
            null,
2558
            $this->encoding
2559
        ),
2560
        $this->encoding
2561
    );
2562
  }
2563
2564
  /**
2565
   * Gets the substring before the first occurrence of a separator.
2566
   * If no match is found returns new empty Stringy object.
2567
   *
2568
   * @param string $separator
2569
   *
2570
   * @return static
2571
   */
2572 View Code Duplication
  public function beforeFirst($separator)
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...
2573
  {
2574
    if ($separator === '') {
2575
      return static::create('');
2576
    }
2577
2578
    if ($this->str === '') {
2579
      return static::create('');
2580
    }
2581
2582
    $offset = $this->indexOf($separator);
2583
    if ($offset === false) {
2584
      return static::create('', $this->encoding);
2585
    }
2586
2587
    return static::create(
2588
        UTF8::substr(
2589
            $this->str,
2590
            0,
2591
            $offset,
2592
            $this->encoding
2593
        ),
2594
        $this->encoding
2595
    );
2596
  }
2597
2598
  /**
2599
   * Gets the substring before the first occurrence of a separator.
2600
   * If no match is found returns new empty Stringy object.
2601
   *
2602
   * @param string $separator
2603
   *
2604
   * @return static
2605
   */
2606 View Code Duplication
  public function beforeFirstIgnoreCase($separator)
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...
2607
  {
2608
    if ($separator === '') {
2609
      return static::create('');
2610
    }
2611
2612
    if ($this->str === '') {
2613
      return static::create('');
2614
    }
2615
2616
    $offset = $this->indexOfIgnoreCase($separator);
2617
    if ($offset === false) {
2618
      return static::create('', $this->encoding);
2619
    }
2620
2621
    return static::create(
2622
        UTF8::substr(
2623
            $this->str,
2624
            0,
2625
            $offset,
2626
            $this->encoding
2627
        ),
2628
        $this->encoding
2629
    );
2630
  }
2631
2632
  /**
2633
   * Gets the substring before the last occurrence of a separator.
2634
   * If no match is found returns new empty Stringy object.
2635
   *
2636
   * @param string $separator
2637
   *
2638
   * @return static
2639
   */
2640 View Code Duplication
  public function beforeLast($separator)
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...
2641
  {
2642
    if ($separator === '') {
2643
      return static::create('');
2644
    }
2645
2646
    if ($this->str === '') {
2647
      return static::create('');
2648
    }
2649
2650
    $offset = $this->indexOfLast($separator);
2651
    if ($offset === false) {
2652
      return static::create('', $this->encoding);
2653
    }
2654
2655
    return static::create(
2656
        UTF8::substr(
2657
            $this->str,
2658
            0,
2659
            $offset,
2660
            $this->encoding
2661
        ),
2662
        $this->encoding
2663
    );
2664
  }
2665
2666
  /**
2667
   * Gets the substring before the last occurrence of a separator.
2668
   * If no match is found returns new empty Stringy object.
2669
   *
2670
   * @param string $separator
2671
   *
2672
   * @return static
2673
   */
2674 View Code Duplication
  public function beforeLastIgnoreCase($separator)
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...
2675
  {
2676
    if ($separator === '') {
2677
      return static::create('');
2678
    }
2679
2680
    if ($this->str === '') {
2681
      return static::create('');
2682
    }
2683
2684
    $offset = $this->indexOfLastIgnoreCase($separator);
2685
    if ($offset === false) {
2686
      return static::create('', $this->encoding);
2687
    }
2688
2689
    return static::create(
2690
        UTF8::substr(
2691
            $this->str,
2692
            0,
2693
            $offset,
2694
            $this->encoding
2695
        ),
2696
        $this->encoding
2697
    );
2698
  }
2699
2700
  /**
2701
   * Returns the string with the first letter of each word capitalized,
2702
   * except for when the word is a name which shouldn't be capitalized.
2703
   *
2704
   * @return static  Object with $str capitalized
2705
   */
2706
  public function capitalizePersonalName()
2707
  {
2708
    $stringy = $this->collapseWhitespace();
2709
    $stringy->str = $this->capitalizePersonalNameByDelimiter($stringy->str, ' ');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->capitalizePersona...ter($stringy->str, ' ') of type this<Stringy\Stringy> is incompatible with the declared type string of property $str.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2710
    $stringy->str = $this->capitalizePersonalNameByDelimiter($stringy->str, '-');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->capitalizePersona...ter($stringy->str, '-') of type this<Stringy\Stringy> is incompatible with the declared type string of property $str.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2711
2712
    return static::create($stringy, $this->encoding);
2713
  }
2714
2715
  /**
2716
   * @param string $word
2717
   *
2718
   * @return string
2719
   */
2720
  protected function capitalizeWord($word)
2721
  {
2722
    $encoding = $this->encoding;
2723
2724
    $firstCharacter = UTF8::substr($word, 0, 1, $encoding);
2725
    $restOfWord = UTF8::substr($word, 1, null, $encoding);
2726
    $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 2724 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...
2727
2728
    return new static($firstCharacterUppercased . $restOfWord, $encoding);
2729
  }
2730
2731
  /**
2732
   * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
2733
   *
2734
   * @param string $names
2735
   * @param string $delimiter
2736
   *
2737
   * @return string
2738
   */
2739
  protected function capitalizePersonalNameByDelimiter($names, $delimiter)
2740
  {
2741
    // init
2742
    $names = explode((string)$delimiter, (string)$names);
2743
    $encoding = $this->encoding;
2744
2745
    $specialCases = array(
2746
        'names'    => array(
2747
            'ab',
2748
            'af',
2749
            'al',
2750
            'and',
2751
            'ap',
2752
            'bint',
2753
            'binte',
2754
            'da',
2755
            'de',
2756
            'del',
2757
            'den',
2758
            'der',
2759
            'di',
2760
            'dit',
2761
            'ibn',
2762
            'la',
2763
            'mac',
2764
            'nic',
2765
            'of',
2766
            'ter',
2767
            'the',
2768
            'und',
2769
            'van',
2770
            'von',
2771
            'y',
2772
            'zu',
2773
        ),
2774
        'prefixes' => array(
2775
            'al-',
2776
            "d'",
2777
            'ff',
2778
            "l'",
2779
            'mac',
2780
            'mc',
2781
            'nic',
2782
        ),
2783
    );
2784
2785
    foreach ($names as &$name) {
2786
      if (in_array($name, $specialCases['names'], true)) {
2787
        continue;
2788
      }
2789
2790
      $continue = false;
2791
2792
      if ($delimiter == '-') {
2793 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...
2794
          if (UTF8::strpos($name, $beginning, null, $encoding) === 0) {
2795
            $continue = true;
2796
          }
2797
        }
2798
      }
2799
2800 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...
2801
        if (UTF8::strpos($name, $beginning, null, $encoding) === 0) {
2802
          $continue = true;
2803
        }
2804
      }
2805
2806
      if ($continue) {
2807
        continue;
2808
      }
2809
2810
      $name = $this->capitalizeWord($name);
2811
    }
2812
2813
    return new static(implode($delimiter, $names), $encoding);
2814
  }
2815
}
2816