Completed
Push — master ( e94879...55b149 )
by Lars
02:59
created

Stringy::ensureLeft()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 10
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 5
cts 5
cp 1
crap 2
1
<?php
2
3
namespace Stringy;
4
5
use voku\helper\AntiXSS;
6
use voku\helper\URLify;
7
use voku\helper\UTF8;
8
9
/**
10
 * Class Stringy
11
 *
12
 * @package Stringy
13
 */
14
class Stringy implements \Countable, \IteratorAggregate, \ArrayAccess
15
{
16
  /**
17
   * An instance's string.
18
   *
19
   * @var string
20
   */
21
  protected $str;
22
23
  /**
24
   * The string's encoding, which should be one of the mbstring module's
25
   * supported encodings.
26
   *
27
   * @var string
28
   */
29
  protected $encoding;
30
31
  /**
32
   * Initializes a Stringy object and assigns both str and encoding properties
33
   * the supplied values. $str is cast to a string prior to assignment, and if
34
   * $encoding is not specified, it defaults to mb_internal_encoding(). Throws
35
   * an InvalidArgumentException if the first argument is an array or object
36
   * without a __toString method.
37
   *
38
   * @param  mixed  $str      Value to modify, after being cast to string
39
   * @param  string $encoding The character encoding
40
   *
41
   * @throws \InvalidArgumentException if an array or object without a
42
   *         __toString method is passed as the first argument
43
   */
44 1020
  public function __construct($str = '', $encoding = null)
45
  {
46 1020
    if (is_array($str)) {
47 1
      throw new \InvalidArgumentException(
48 1
          'Passed value cannot be an array'
49
      );
50 1019
    } elseif (is_object($str) && !method_exists($str, '__toString')) {
51 1
      throw new \InvalidArgumentException(
52 1
          'Passed object must have a __toString method'
53
      );
54
    }
55
56
    // don't throw a notice on PHP 5.3
57 1018
    if (!defined('ENT_SUBSTITUTE')) {
58
      define('ENT_SUBSTITUTE', 8);
59
    }
60
61
    // init
62 1018
    UTF8::checkForSupport();
63 1018
    $this->str = (string)$str;
64
65 1018
    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...
66 806
      $this->encoding = $encoding;
67
    } else {
68 650
      UTF8::mbstring_loaded();
69 650
      $this->encoding = mb_internal_encoding();
70
    }
71
72 1018
    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...
73 806
      $this->encoding = $encoding;
74
    } else {
75 650
      $this->encoding = mb_internal_encoding();
76
    }
77 1018
  }
78
79
  /**
80
   * Returns the value in $str.
81
   *
82
   * @return string The current value of the $str property
83
   */
84 111
  public function __toString()
85
  {
86 111
    return $this->str;
87
  }
88
89
  /**
90
   * Returns a new string with $string appended.
91
   *
92
   * @param  string $string The string to append
93
   *
94
   * @return Stringy Object with appended $string
95
   */
96 5
  public function append($string)
97
  {
98 5
    return static::create($this->str . $string, $this->encoding);
99
  }
100
101
  /**
102
   * Append an password (limited to chars that are good readable).
103
   *
104
   * @param int $length length of the random string
105
   *
106
   * @return Stringy Object with appended password
107
   */
108 1
  public function appendPassword($length)
109
  {
110 1
    $possibleChars = '2346789bcdfghjkmnpqrtvwxyzBCDFGHJKLMNPQRTVWXYZ';
111
112 1
    return $this->appendRandomString($length, $possibleChars);
113
  }
114
115
  /**
116
   * Append an unique identifier.
117
   *
118
   * @param string|int $extraPrefix
119
   *
120
   * @return Stringy Object with appended unique identifier as md5-hash
121
   */
122 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...
123
  {
124 1
    $prefix = mt_rand() .
125 1
              session_id() .
126 1
              (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '') .
127 1
              (isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '') .
128 1
              $extraPrefix;
129
130 1
    return $this->append(md5(uniqid($prefix, true) . $prefix));
131
  }
132
133
  /**
134
   * Append an random string.
135
   *
136
   * @param int    $length        length of the random string
137
   * @param string $possibleChars characters string for the random selection
138
   *
139
   * @return Stringy Object with appended random string
140
   */
141 2
  public function appendRandomString($length, $possibleChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
142
  {
143
    // init
144 2
    $i = 0;
145 2
    $length = (int)$length;
146 2
    $str = $this->str;
147 2
    $maxlength = UTF8::strlen($possibleChars, $this->encoding);
148
149 2
    if ($maxlength === 0) {
150 1
      return $this;
151
    }
152
153
    // add random chars
154 2
    while ($i < $length) {
155 2
      $char = UTF8::substr($possibleChars, mt_rand(0, $maxlength - 1), 1, $this->encoding);
156 2
      $str .= $char;
157 2
      $i++;
158
    }
159
160 2
    return $this->append($str);
161
  }
162
163
  /**
164
   * Creates a Stringy object and assigns both str and encoding properties
165
   * the supplied values. $str is cast to a string prior to assignment, and if
166
   * $encoding is not specified, it defaults to mb_internal_encoding(). It
167
   * then returns the initialized object. Throws an InvalidArgumentException
168
   * if the first argument is an array or object without a __toString method.
169
   *
170
   * @param  mixed  $str      Value to modify, after being cast to string
171
   * @param  string $encoding The character encoding
172
   *
173
   * @return Stringy A Stringy object
174
   * @throws \InvalidArgumentException if an array or object without a
175
   *         __toString method is passed as the first argument
176
   */
177 1010
  public static function create($str = '', $encoding = null)
178
  {
179 1010
    return new static($str, $encoding);
180
  }
181
182
  /**
183
   * Returns the substring between $start and $end, if found, or an empty
184
   * string. An optional offset may be supplied from which to begin the
185
   * search for the start string.
186
   *
187
   * @param  string $start  Delimiter marking the start of the substring
188
   * @param  string $end    Delimiter marking the end of the substring
189
   * @param  int    $offset Index from which to begin the search
190
   *
191
   * @return Stringy Object whose $str is a substring between $start and $end
192
   */
193 16
  public function between($start, $end, $offset = 0)
194
  {
195 16
    $startIndex = $this->indexOf($start, $offset);
196 16
    if ($startIndex === false) {
197 2
      return static::create('', $this->encoding);
198
    }
199
200 14
    $substrIndex = $startIndex + UTF8::strlen($start, $this->encoding);
201 14
    $endIndex = $this->indexOf($end, $substrIndex);
202 14
    if ($endIndex === false) {
203 2
      return static::create('', $this->encoding);
204
    }
205
206 12
    return $this->substr($substrIndex, $endIndex - $substrIndex);
207
  }
208
209
  /**
210
   * Returns the index of the first occurrence of $needle in the string,
211
   * and false if not found. Accepts an optional offset from which to begin
212
   * the search.
213
   *
214
   * @param  string $needle Substring to look for
215
   * @param  int    $offset Offset from which to search
216
   *
217
   * @return int|bool The occurrence's index if found, otherwise false
218
   */
219 28
  public function indexOf($needle, $offset = 0)
220
  {
221 28
    return UTF8::strpos($this->str, (string)$needle, (int)$offset, $this->encoding);
222
  }
223
224
  /**
225
   * Returns the substring beginning at $start with the specified $length.
226
   * It differs from the UTF8::substr() function in that providing a $length of
227
   * null will return the rest of the string, rather than an empty string.
228
   *
229
   * @param  int $start  Position of the first character to use
230
   * @param  int $length Maximum number of characters used
231
   *
232
   * @return Stringy Object with its $str being the substring
233
   */
234 64
  public function substr($start, $length = null)
235
  {
236 64
    if ($length === null) {
237 19
      $length = $this->length();
238
    }
239
240 64
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
241
242 64
    return static::create($str, $this->encoding);
243
  }
244
245
  /**
246
   * Returns the length of the string.
247
   *
248
   * @return int The number of characters in $str given the encoding
249
   */
250 247
  public function length()
251
  {
252 247
    return UTF8::strlen($this->str, $this->encoding);
253
  }
254
255
  /**
256
   * Trims the string and replaces consecutive whitespace characters with a
257
   * single space. This includes tabs and newline characters, as well as
258
   * multibyte whitespace such as the thin space and ideographic space.
259
   *
260
   * @return Stringy Object with a trimmed $str and condensed whitespace
261
   */
262 13
  public function collapseWhitespace()
263
  {
264 13
    return $this->regexReplace('[[:space:]]+', ' ')->trim();
265
  }
266
267
  /**
268
   * Returns a string with whitespace removed from the start and end of the
269
   * string. Supports the removal of unicode whitespace. Accepts an optional
270
   * string of characters to strip instead of the defaults.
271
   *
272
   * @param  string $chars Optional string of characters to strip
273
   *
274
   * @return Stringy Object with a trimmed $str
275
   */
276 114 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...
277
  {
278 114
    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...
279 113
      $chars = '[:space:]';
280
    } else {
281 1
      $chars = preg_quote($chars, '/');
282
    }
283
284 114
    return $this->regexReplace("^[$chars]+|[$chars]+\$", '');
285
  }
286
287
  /**
288
   * Replaces all occurrences of $pattern in $str by $replacement.
289
   *
290
   * @param  string $pattern     The regular expression pattern
291
   * @param  string $replacement The string to replace with
292
   * @param  string $options     Matching conditions to be used
293
   *
294
   * @return Stringy Object with the result2ing $str after the replacements
295
   */
296 184
  public function regexReplace($pattern, $replacement, $options = '')
297
  {
298 184
    if ($options === 'msr') {
299 8
      $options = 'ms';
300
    }
301
302 184
    $str = preg_replace(
303 184
        '/' . $pattern . '/u' . $options,
304
        $replacement,
305 184
        $this->str
306
    );
307
308 184
    return static::create($str, $this->encoding);
309
  }
310
311
  /**
312
   * Returns true if the string contains all $needles, false otherwise. By
313
   * default the comparison is case-sensitive, but can be made insensitive by
314
   * setting $caseSensitive to false.
315
   *
316
   * @param  array $needles       SubStrings to look for
317
   * @param  bool  $caseSensitive Whether or not to enforce case-sensitivity
318
   *
319
   * @return bool   Whether or not $str contains $needle
320
   */
321 43 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...
322
  {
323
    /** @noinspection IsEmptyFunctionUsageInspection */
324 43
    if (empty($needles)) {
325 1
      return false;
326
    }
327
328 42
    foreach ($needles as $needle) {
329 42
      if (!$this->contains($needle, $caseSensitive)) {
330 42
        return false;
331
      }
332
    }
333
334 24
    return true;
335
  }
336
337
  /**
338
   * Returns true if the string contains $needle, false otherwise. By default
339
   * the comparison is case-sensitive, but can be made insensitive by setting
340
   * $caseSensitive to false.
341
   *
342
   * @param  string $needle        Substring to look for
343
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
344
   *
345
   * @return bool   Whether or not $str contains $needle
346
   */
347 105
  public function contains($needle, $caseSensitive = true)
348
  {
349 105
    $encoding = $this->encoding;
350
351 105
    if ($caseSensitive) {
352 55
      return (UTF8::strpos($this->str, $needle, 0, $encoding) !== false);
353
    } else {
354 50
      return (UTF8::stripos($this->str, $needle, 0, $encoding) !== false);
355
    }
356
  }
357
358
  /**
359
   * Returns true if the string contains any $needles, false otherwise. By
360
   * default the comparison is case-sensitive, but can be made insensitive by
361
   * setting $caseSensitive to false.
362
   *
363
   * @param  array $needles       SubStrings to look for
364
   * @param  bool  $caseSensitive Whether or not to enforce case-sensitivity
365
   *
366
   * @return bool   Whether or not $str contains $needle
367
   */
368 43 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...
369
  {
370
    /** @noinspection IsEmptyFunctionUsageInspection */
371 43
    if (empty($needles)) {
372 1
      return false;
373
    }
374
375 42
    foreach ($needles as $needle) {
376 42
      if ($this->contains($needle, $caseSensitive)) {
377 42
        return true;
378
      }
379
    }
380
381 18
    return false;
382
  }
383
384
  /**
385
   * Returns the length of the string, implementing the countable interface.
386
   *
387
   * @return int The number of characters in the string, given the encoding
388
   */
389 1
  public function count()
390
  {
391 1
    return $this->length();
392
  }
393
394
  /**
395
   * Returns the number of occurrences of $substring in the given string.
396
   * By default, the comparison is case-sensitive, but can be made insensitive
397
   * by setting $caseSensitive to false.
398
   *
399
   * @param  string $substring     The substring to search for
400
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
401
   *
402
   * @return int    The number of $substring occurrences
403
   */
404 15
  public function countSubstr($substring, $caseSensitive = true)
405
  {
406 15
    if ($caseSensitive) {
407 9
      return UTF8::substr_count($this->str, $substring, 0, null, $this->encoding);
408
    }
409
410 6
    $str = UTF8::strtoupper($this->str, $this->encoding);
411 6
    $substring = UTF8::strtoupper($substring, $this->encoding);
412
413 6
    return UTF8::substr_count($str, $substring, 0, null, $this->encoding);
414
  }
415
416
  /**
417
   * Returns a lowercase and trimmed string separated by dashes. Dashes are
418
   * inserted before uppercase characters (with the exception of the first
419
   * character of the string), and in place of spaces as well as underscores.
420
   *
421
   * @return Stringy Object with a dasherized $str
422
   */
423 19
  public function dasherize()
424
  {
425 19
    return $this->delimit('-');
426
  }
427
428
  /**
429
   * Returns a lowercase and trimmed string separated by the given delimiter.
430
   * Delimiters are inserted before uppercase characters (with the exception
431
   * of the first character of the string), and in place of spaces, dashes,
432
   * and underscores. Alpha delimiters are not converted to lowercase.
433
   *
434
   * @param  string $delimiter Sequence used to separate parts of the string
435
   *
436
   * @return Stringy Object with a delimited $str
437
   */
438 49
  public function delimit($delimiter)
439
  {
440 49
    $str = $this->trim();
441
442 49
    $str = preg_replace('/\B([A-Z])/u', '-\1', $str);
443
444 49
    $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...
445
446 49
    $str = preg_replace('/[-_\s]+/u', $delimiter, $str);
447
448 49
    return static::create($str, $this->encoding);
449
  }
450
451
  /**
452
   * Ensures that the string begins with $substring. If it doesn't, it's
453
   * prepended.
454
   *
455
   * @param  string $substring The substring to add if not present
456
   *
457
   * @return Stringy Object with its $str prefixed by the $substring
458
   */
459 10 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...
460
  {
461 10
    $stringy = static::create($this->str, $this->encoding);
462
463 10
    if (!$stringy->startsWith($substring)) {
464 4
      $stringy->str = $substring . $stringy->str;
465
    }
466
467 10
    return $stringy;
468
  }
469
470
  /**
471
   * Returns true if the string begins with $substring, false otherwise. By
472
   * default, the comparison is case-sensitive, but can be made insensitive
473
   * by setting $caseSensitive to false.
474
   *
475
   * @param  string $substring     The substring to look for
476
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
477
   *
478
   * @return bool   Whether or not $str starts with $substring
479
   */
480 33
  public function startsWith($substring, $caseSensitive = true)
481
  {
482 33
    $str = $this->str;
483
484 33 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...
485 4
      $substring = UTF8::strtolower($substring, $this->encoding);
486 4
      $str = UTF8::strtolower($this->str, $this->encoding);
487
    }
488
489 33
    return UTF8::strpos($str, $substring, $this->encoding) === 0;
490
  }
491
492
  /**
493
   * Ensures that the string ends with $substring. If it doesn't, it's
494
   * appended.
495
   *
496
   * @param  string $substring The substring to add if not present
497
   *
498
   * @return Stringy Object with its $str suffixed by the $substring
499
   */
500 10 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...
501
  {
502 10
    $stringy = static::create($this->str, $this->encoding);
503
504 10
    if (!$stringy->endsWith($substring)) {
505 4
      $stringy->str .= $substring;
506
    }
507
508 10
    return $stringy;
509
  }
510
511
  /**
512
   * Returns true if the string ends with $substring, false otherwise. By
513
   * default, the comparison is case-sensitive, but can be made insensitive
514
   * by setting $caseSensitive to false.
515
   *
516
   * @param  string $substring     The substring to look for
517
   * @param  bool   $caseSensitive Whether or not to enforce case-sensitivity
518
   *
519
   * @return bool   Whether or not $str ends with $substring
520
   */
521 33
  public function endsWith($substring, $caseSensitive = true)
522
  {
523 33
    $substringLength = UTF8::strlen($substring, $this->encoding);
524 33
    $strLength = $this->length();
525
526 33
    $endOfStr = UTF8::substr(
527 33
        $this->str,
528 33
        $strLength - $substringLength,
529
        $substringLength,
530 33
        $this->encoding
531
    );
532
533 33 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...
534 4
      $substring = UTF8::strtolower($substring, $this->encoding);
535 4
      $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...
536
    }
537
538 33
    return (string)$substring === $endOfStr;
539
  }
540
541
  /**
542
   * Returns the first $n characters of the string.
543
   *
544
   * @param  int $n Number of characters to retrieve from the start
545
   *
546
   * @return Stringy Object with its $str being the first $n chars
547
   */
548 12 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...
549
  {
550 12
    $stringy = static::create($this->str, $this->encoding);
551
552 12
    if ($n < 0) {
553 2
      $stringy->str = '';
554
    } else {
555 10
      return $stringy->substr(0, $n);
556
    }
557
558 2
    return $stringy;
559
  }
560
561
  /**
562
   * Returns the encoding used by the Stringy object.
563
   *
564
   * @return string The current value of the $encoding property
565
   */
566 3
  public function getEncoding()
567
  {
568 3
    return $this->encoding;
569
  }
570
571
  /**
572
   * Returns a new ArrayIterator, thus implementing the IteratorAggregate
573
   * interface. The ArrayIterator's constructor is passed an array of chars
574
   * in the multibyte string. This enables the use of foreach with instances
575
   * of Stringy\Stringy.
576
   *
577
   * @return \ArrayIterator An iterator for the characters in the string
578
   */
579 1
  public function getIterator()
580
  {
581 1
    return new \ArrayIterator($this->chars());
582
  }
583
584
  /**
585
   * Returns an array consisting of the characters in the string.
586
   *
587
   * @return array An array of string chars
588
   */
589 4
  public function chars()
590
  {
591
    // init
592 4
    $chars = array();
593 4
    $l = $this->length();
594
595 4
    for ($i = 0; $i < $l; $i++) {
596 3
      $chars[] = $this->at($i)->str;
597
    }
598
599 4
    return $chars;
600
  }
601
602
  /**
603
   * Returns the character at $index, with indexes starting at 0.
604
   *
605
   * @param  int $index Position of the character
606
   *
607
   * @return Stringy The character at $index
608
   */
609 11
  public function at($index)
610
  {
611 11
    return $this->substr($index, 1);
612
  }
613
614
  /**
615
   * Returns true if the string contains a lower case char, false
616
   * otherwise.
617
   *
618
   * @return bool Whether or not the string contains a lower case character.
619
   */
620 12
  public function hasLowerCase()
621
  {
622 12
    return $this->matchesPattern('.*[[:lower:]]');
623
  }
624
625
  /**
626
   * Returns true if $str matches the supplied pattern, false otherwise.
627
   *
628
   * @param  string $pattern Regex pattern to match against
629
   *
630
   * @return bool   Whether or not $str matches the pattern
631
   */
632 91
  private function matchesPattern($pattern)
633
  {
634 91
    if (preg_match('/' . $pattern . '/u', $this->str)) {
635 54
      return true;
636
    } else {
637 37
      return false;
638
    }
639
  }
640
641
  /**
642
   * Returns true if the string contains an upper case char, false
643
   * otherwise.
644
   *
645
   * @return bool Whether or not the string contains an upper case character.
646
   */
647 12
  public function hasUpperCase()
648
  {
649 12
    return $this->matchesPattern('.*[[:upper:]]');
650
  }
651
652
  /**
653
   * Convert all HTML entities to their applicable characters.
654
   *
655
   * @param  int|null $flags Optional flags
656
   *
657
   * @return Stringy  Object with the resulting $str after being html decoded.
658
   */
659 5
  public function htmlDecode($flags = ENT_COMPAT)
660
  {
661 5
    $str = UTF8::html_entity_decode($this->str, $flags, $this->encoding);
662
663 5
    return static::create($str, $this->encoding);
664
  }
665
666
  /**
667
   * Convert all applicable characters to HTML entities.
668
   *
669
   * @param  int|null $flags Optional flags
670
   *
671
   * @return Stringy  Object with the resulting $str after being html encoded.
672
   */
673 5
  public function htmlEncode($flags = ENT_COMPAT)
674
  {
675 5
    $str = UTF8::htmlentities($this->str, $flags, $this->encoding);
676
677 5
    return static::create($str, $this->encoding);
678
  }
679
680
  /**
681
   * Capitalizes the first word of the string, replaces underscores with
682
   * spaces, and strips '_id'.
683
   *
684
   * @return Stringy Object with a humanized $str
685
   */
686 3
  public function humanize()
687
  {
688 3
    $str = UTF8::str_replace(array('_id', '_'), array('', ' '), $this->str);
689
690 3
    return static::create($str, $this->encoding)->trim()->upperCaseFirst();
691
  }
692
693
  /**
694
   * Converts the first character of the supplied string to upper case.
695
   *
696
   * @return Stringy Object with the first character of $str being upper case
697
   */
698 27 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...
699
  {
700 27
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
701 27
    $rest = UTF8::substr(
702 27
        $this->str,
703 27
        1,
704 27
        $this->length() - 1,
705 27
        $this->encoding
706
    );
707
708 27
    $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 700 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...
709
710 27
    return static::create($str, $this->encoding);
711
  }
712
713
  /**
714
   * Returns the index of the last occurrence of $needle in the string,
715
   * and false if not found. Accepts an optional offset from which to begin
716
   * the search. Offsets may be negative to count from the last character
717
   * in the string.
718
   *
719
   * @param  string $needle Substring to look for
720
   * @param  int    $offset Offset from which to search
721
   *
722
   * @return int|bool The last occurrence's index if found, otherwise false
723
   */
724 12
  public function indexOfLast($needle, $offset = 0)
725
  {
726 12
    return UTF8::strrpos($this->str, (string)$needle, (int)$offset, $this->encoding);
727
  }
728
729
  /**
730
   * Inserts $substring into the string at the $index provided.
731
   *
732
   * @param  string $substring String to be inserted
733
   * @param  int    $index     The index at which to insert the substring
734
   *
735
   * @return Stringy Object with the resulting $str after the insertion
736
   */
737 8 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...
738
  {
739 8
    $stringy = static::create($this->str, $this->encoding);
740 8
    if ($index > $stringy->length()) {
741 1
      return $stringy;
742
    }
743
744 7
    $start = UTF8::substr($stringy->str, 0, $index, $stringy->encoding);
745 7
    $end = UTF8::substr($stringy->str, $index, $stringy->length(), $stringy->encoding);
746
747 7
    $stringy->str = $start . $substring . $end;
748
749 7
    return $stringy;
750
  }
751
752
  /**
753
   * Returns true if the string contains only alphabetic chars, false
754
   * otherwise.
755
   *
756
   * @return bool Whether or not $str contains only alphabetic chars
757
   */
758 10
  public function isAlpha()
759
  {
760 10
    return $this->matchesPattern('^[[:alpha:]]*$');
761
  }
762
763
  /**
764
   * Determine whether the string is considered to be empty.
765
   *
766
   * A variable is considered empty if it does not exist or if its value equals FALSE.
767
   * empty() does not generate a warning if the variable does not exist.
768
   *
769
   * @return bool
770
   */
771
  public function isEmpty()
772
  {
773
    return empty($this->str);
774
  }
775
776
  /**
777
   * Returns true if the string contains only alphabetic and numeric chars,
778
   * false otherwise.
779
   *
780
   * @return bool Whether or not $str contains only alphanumeric chars
781
   */
782 13
  public function isAlphanumeric()
783
  {
784 13
    return $this->matchesPattern('^[[:alnum:]]*$');
785
  }
786
787
  /**
788
   * Returns true if the string contains only whitespace chars, false
789
   * otherwise.
790
   *
791
   * @return bool Whether or not $str contains only whitespace characters
792
   */
793 15
  public function isBlank()
794
  {
795 15
    return $this->matchesPattern('^[[:space:]]*$');
796
  }
797
798
  /**
799
   * Returns true if the string contains only hexadecimal chars, false
800
   * otherwise.
801
   *
802
   * @return bool Whether or not $str contains only hexadecimal chars
803
   */
804 13
  public function isHexadecimal()
805
  {
806 13
    return $this->matchesPattern('^[[:xdigit:]]*$');
807
  }
808
809
  /**
810
   * Returns true if the string contains HTML-Tags, false otherwise.
811
   *
812
   * @return bool Whether or not $str contains HTML-Tags
813
   */
814 1
  public function isHtml()
815
  {
816 1
    return UTF8::isHtml($this->str);
817
  }
818
819
  /**
820
   * Returns true if the string is JSON, false otherwise. Unlike json_decode
821
   * in PHP 5.x, this method is consistent with PHP 7 and other JSON parsers,
822
   * in that an empty string is not considered valid JSON.
823
   *
824
   * @return bool Whether or not $str is JSON
825
   */
826 20
  public function isJson()
827
  {
828 20
    if (!isset($this->str[0])) {
829 1
      return false;
830
    }
831
832 19
    json_decode($this->str);
833
834 19
    if (json_last_error() === JSON_ERROR_NONE) {
835 11
      return true;
836
    } else {
837 8
      return false;
838
    }
839
  }
840
841
  /**
842
   * Returns true if the string contains only lower case chars, false
843
   * otherwise.
844
   *
845
   * @return bool Whether or not $str contains only lower case characters
846
   */
847 8
  public function isLowerCase()
848
  {
849 8
    if ($this->matchesPattern('^[[:lower:]]*$')) {
850 3
      return true;
851
    } else {
852 5
      return false;
853
    }
854
  }
855
856
  /**
857
   * Returns true if the string is serialized, false otherwise.
858
   *
859
   * @return bool Whether or not $str is serialized
860
   */
861 7
  public function isSerialized()
862
  {
863 7
    if (!isset($this->str[0])) {
864 1
      return false;
865
    }
866
867
    /** @noinspection PhpUsageOfSilenceOperatorInspection */
868
    if (
869 6
        $this->str === 'b:0;'
870
        ||
871 6
        @unserialize($this->str) !== false
872
    ) {
873 4
      return true;
874
    } else {
875 2
      return false;
876
    }
877
  }
878
879
  /**
880
   * Returns true if the string contains only lower case chars, false
881
   * otherwise.
882
   *
883
   * @return bool Whether or not $str contains only lower case characters
884
   */
885 8
  public function isUpperCase()
886
  {
887 8
    return $this->matchesPattern('^[[:upper:]]*$');
888
  }
889
890
  /**
891
   * Returns the last $n characters of the string.
892
   *
893
   * @param  int $n Number of characters to retrieve from the end
894
   *
895
   * @return Stringy Object with its $str being the last $n chars
896
   */
897 12 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...
898
  {
899 12
    $stringy = static::create($this->str, $this->encoding);
900
901 12
    if ($n <= 0) {
902 4
      $stringy->str = '';
903
    } else {
904 8
      return $stringy->substr(-$n);
905
    }
906
907 4
    return $stringy;
908
  }
909
910
  /**
911
   * Splits on newlines and carriage returns, returning an array of Stringy
912
   * objects corresponding to the lines in the string.
913
   *
914
   * @return Stringy[] An array of Stringy objects
915
   */
916 15
  public function lines()
917
  {
918 15
    $array = preg_split('/[\r\n]{1,2}/u', $this->str);
919
    /** @noinspection CallableInLoopTerminationConditionInspection */
920
    /** @noinspection ForeachInvariantsInspection */
921 15 View Code Duplication
    for ($i = 0; $i < count($array); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

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

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

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
922 15
      $array[$i] = static::create($array[$i], $this->encoding);
923
    }
924
925 15
    return $array;
926
  }
927
928
  /**
929
   * Returns the longest common prefix between the string and $otherStr.
930
   *
931
   * @param  string $otherStr Second string for comparison
932
   *
933
   * @return Stringy Object with its $str being the longest common prefix
934
   */
935 10
  public function longestCommonPrefix($otherStr)
936
  {
937 10
    $encoding = $this->encoding;
938 10
    $maxLength = min($this->length(), UTF8::strlen($otherStr, $encoding));
939
940 10
    $longestCommonPrefix = '';
941 10
    for ($i = 0; $i < $maxLength; $i++) {
942 8
      $char = UTF8::substr($this->str, $i, 1, $encoding);
943
944 8
      if ($char == UTF8::substr($otherStr, $i, 1, $encoding)) {
945 6
        $longestCommonPrefix .= $char;
946
      } else {
947 6
        break;
948
      }
949
    }
950
951 10
    return static::create($longestCommonPrefix, $encoding);
952
  }
953
954
  /**
955
   * Returns the longest common suffix between the string and $otherStr.
956
   *
957
   * @param  string $otherStr Second string for comparison
958
   *
959
   * @return Stringy Object with its $str being the longest common suffix
960
   */
961 10
  public function longestCommonSuffix($otherStr)
962
  {
963 10
    $encoding = $this->encoding;
964 10
    $maxLength = min($this->length(), UTF8::strlen($otherStr, $encoding));
965
966 10
    $longestCommonSuffix = '';
967 10
    for ($i = 1; $i <= $maxLength; $i++) {
968 8
      $char = UTF8::substr($this->str, -$i, 1, $encoding);
969
970 8
      if ($char == UTF8::substr($otherStr, -$i, 1, $encoding)) {
971 6
        $longestCommonSuffix = $char . $longestCommonSuffix;
972
      } else {
973 6
        break;
974
      }
975
    }
976
977 10
    return static::create($longestCommonSuffix, $encoding);
978
  }
979
980
  /**
981
   * Returns the longest common substring between the string and $otherStr.
982
   * In the case of ties, it returns that which occurs first.
983
   *
984
   * @param  string $otherStr Second string for comparison
985
   *
986
   * @return Stringy Object with its $str being the longest common substring
987
   */
988 10
  public function longestCommonSubstring($otherStr)
989
  {
990
    // Uses dynamic programming to solve
991
    // http://en.wikipedia.org/wiki/Longest_common_substring_problem
992 10
    $encoding = $this->encoding;
993 10
    $stringy = static::create($this->str, $encoding);
994 10
    $strLength = $stringy->length();
995 10
    $otherLength = UTF8::strlen($otherStr, $encoding);
996
997
    // Return if either string is empty
998 10
    if ($strLength == 0 || $otherLength == 0) {
999 2
      $stringy->str = '';
1000
1001 2
      return $stringy;
1002
    }
1003
1004 8
    $len = 0;
1005 8
    $end = 0;
1006 8
    $table = array_fill(
1007 8
        0, $strLength + 1,
1008 8
        array_fill(0, $otherLength + 1, 0)
1009
    );
1010
1011 8
    for ($i = 1; $i <= $strLength; $i++) {
1012 8
      for ($j = 1; $j <= $otherLength; $j++) {
1013 8
        $strChar = UTF8::substr($stringy->str, $i - 1, 1, $encoding);
1014 8
        $otherChar = UTF8::substr($otherStr, $j - 1, 1, $encoding);
1015
1016 8
        if ($strChar == $otherChar) {
1017 8
          $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
1018 8
          if ($table[$i][$j] > $len) {
1019 8
            $len = $table[$i][$j];
1020 8
            $end = $i;
1021
          }
1022
        } else {
1023 8
          $table[$i][$j] = 0;
1024
        }
1025
      }
1026
    }
1027
1028 8
    $stringy->str = UTF8::substr($stringy->str, $end - $len, $len, $encoding);
0 ignored issues
show
Documentation Bug introduced by
It seems like \voku\helper\UTF8::subst... $len, $len, $encoding) can also be of type false. However, the property $str is declared as type string. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1029
1030 8
    return $stringy;
1031
  }
1032
1033
  /**
1034
   * Returns whether or not a character exists at an index. Offsets may be
1035
   * negative to count from the last character in the string. Implements
1036
   * part of the ArrayAccess interface.
1037
   *
1038
   * @param  mixed $offset The index to check
1039
   *
1040
   * @return boolean Whether or not the index exists
1041
   */
1042 6
  public function offsetExists($offset)
1043
  {
1044
    // init
1045 6
    $length = $this->length();
1046 6
    $offset = (int)$offset;
1047
1048 6
    if ($offset >= 0) {
1049 3
      return ($length > $offset);
1050
    }
1051
1052 3
    return ($length >= abs($offset));
1053
  }
1054
1055
  /**
1056
   * Returns the character at the given index. Offsets may be negative to
1057
   * count from the last character in the string. Implements part of the
1058
   * ArrayAccess interface, and throws an OutOfBoundsException if the index
1059
   * does not exist.
1060
   *
1061
   * @param  mixed $offset The index from which to retrieve the char
1062
   *
1063
   * @return string                 The character at the specified index
1064
   * @throws \OutOfBoundsException If the positive or negative offset does
1065
   *                               not exist
1066
   */
1067 2
  public function offsetGet($offset)
1068
  {
1069
    // init
1070 2
    $offset = (int)$offset;
1071 2
    $length = $this->length();
1072
1073
    if (
1074 2
        ($offset >= 0 && $length <= $offset)
1075
        ||
1076 2
        $length < abs($offset)
1077
    ) {
1078 1
      throw new \OutOfBoundsException('No character exists at the index');
1079
    }
1080
1081 1
    return UTF8::substr($this->str, $offset, 1, $this->encoding);
1082
  }
1083
1084
  /**
1085
   * Implements part of the ArrayAccess interface, but throws an exception
1086
   * when called. This maintains the immutability of Stringy objects.
1087
   *
1088
   * @param  mixed $offset The index of the character
1089
   * @param  mixed $value  Value to set
1090
   *
1091
   * @throws \Exception When called
1092
   */
1093 1
  public function offsetSet($offset, $value)
1094
  {
1095
    // Stringy is immutable, cannot directly set char
1096 1
    throw new \Exception('Stringy object is immutable, cannot modify char');
1097
  }
1098
1099
  /**
1100
   * Implements part of the ArrayAccess interface, but throws an exception
1101
   * when called. This maintains the immutability of Stringy objects.
1102
   *
1103
   * @param  mixed $offset The index of the character
1104
   *
1105
   * @throws \Exception When called
1106
   */
1107 1
  public function offsetUnset($offset)
1108
  {
1109
    // Don't allow directly modifying the string
1110 1
    throw new \Exception('Stringy object is immutable, cannot unset char');
1111
  }
1112
1113
  /**
1114
   * Pads the string to a given length with $padStr. If length is less than
1115
   * or equal to the length of the string, no padding takes places. The
1116
   * default string used for padding is a space, and the default type (one of
1117
   * 'left', 'right', 'both') is 'right'. Throws an InvalidArgumentException
1118
   * if $padType isn't one of those 3 values.
1119
   *
1120
   * @param  int    $length  Desired string length after padding
1121
   * @param  string $padStr  String used to pad, defaults to space
1122
   * @param  string $padType One of 'left', 'right', 'both'
1123
   *
1124
   * @return Stringy Object with a padded $str
1125
   * @throws \InvalidArgumentException If $padType isn't one of 'right', 'left' or 'both'
1126
   */
1127 13
  public function pad($length, $padStr = ' ', $padType = 'right')
1128
  {
1129 13
    if (!in_array($padType, array('left', 'right', 'both'), true)) {
1130 1
      throw new \InvalidArgumentException(
1131 1
          'Pad expects $padType ' . "to be one of 'left', 'right' or 'both'"
1132
      );
1133
    }
1134
1135
    switch ($padType) {
1136 12
      case 'left':
1137 3
        return $this->padLeft($length, $padStr);
1138 9
      case 'right':
1139 6
        return $this->padRight($length, $padStr);
1140
      default:
1141 3
        return $this->padBoth($length, $padStr);
1142
    }
1143
  }
1144
1145
  /**
1146
   * Returns a new string of a given length such that the beginning of the
1147
   * string is padded. Alias for pad() with a $padType of 'left'.
1148
   *
1149
   * @param  int    $length Desired string length after padding
1150
   * @param  string $padStr String used to pad, defaults to space
1151
   *
1152
   * @return Stringy String with left padding
1153
   */
1154 10
  public function padLeft($length, $padStr = ' ')
1155
  {
1156 10
    return $this->applyPadding($length - $this->length(), 0, $padStr);
1157
  }
1158
1159
  /**
1160
   * Adds the specified amount of left and right padding to the given string.
1161
   * The default character used is a space.
1162
   *
1163
   * @param  int    $left   Length of left padding
1164
   * @param  int    $right  Length of right padding
1165
   * @param  string $padStr String used to pad
1166
   *
1167
   * @return Stringy String with padding applied
1168
   */
1169 37
  private function applyPadding($left = 0, $right = 0, $padStr = ' ')
1170
  {
1171 37
    $stringy = static::create($this->str, $this->encoding);
1172
1173 37
    $length = UTF8::strlen($padStr, $stringy->encoding);
1174
1175 37
    $strLength = $stringy->length();
1176 37
    $paddedLength = $strLength + $left + $right;
1177
1178 37
    if (!$length || $paddedLength <= $strLength) {
1179 3
      return $stringy;
1180
    }
1181
1182 34
    $leftPadding = UTF8::substr(
1183 34
        UTF8::str_repeat(
1184
            $padStr,
1185 34
            ceil($left / $length)
1186
        ),
1187 34
        0,
1188
        $left,
1189 34
        $stringy->encoding
1190
    );
1191
1192 34
    $rightPadding = UTF8::substr(
1193 34
        UTF8::str_repeat(
1194
            $padStr,
1195 34
            ceil($right / $length)
1196
        ),
1197 34
        0,
1198
        $right,
1199 34
        $stringy->encoding
1200
    );
1201
1202 34
    $stringy->str = $leftPadding . $stringy->str . $rightPadding;
1203
1204 34
    return $stringy;
1205
  }
1206
1207
  /**
1208
   * Returns a new string of a given length such that the end of the string
1209
   * is padded. Alias for pad() with a $padType of 'right'.
1210
   *
1211
   * @param  int    $length Desired string length after padding
1212
   * @param  string $padStr String used to pad, defaults to space
1213
   *
1214
   * @return Stringy String with right padding
1215
   */
1216 13
  public function padRight($length, $padStr = ' ')
1217
  {
1218 13
    return $this->applyPadding(0, $length - $this->length(), $padStr);
1219
  }
1220
1221
  /**
1222
   * Returns a new string of a given length such that both sides of the
1223
   * string are padded. Alias for pad() with a $padType of 'both'.
1224
   *
1225
   * @param  int    $length Desired string length after padding
1226
   * @param  string $padStr String used to pad, defaults to space
1227
   *
1228
   * @return Stringy String with padding applied
1229
   */
1230 14
  public function padBoth($length, $padStr = ' ')
1231
  {
1232 14
    $padding = $length - $this->length();
1233
1234 14
    return $this->applyPadding(floor($padding / 2), ceil($padding / 2), $padStr);
1235
  }
1236
1237
  /**
1238
   * Returns a new string starting with $string.
1239
   *
1240
   * @param  string $string The string to append
1241
   *
1242
   * @return Stringy Object with appended $string
1243
   */
1244 2
  public function prepend($string)
1245
  {
1246 2
    return static::create($string . $this->str, $this->encoding);
1247
  }
1248
1249
  /**
1250
   * Returns a new string with the prefix $substring removed, if present.
1251
   *
1252
   * @param  string $substring The prefix to remove
1253
   *
1254
   * @return Stringy Object having a $str without the prefix $substring
1255
   */
1256 12 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...
1257
  {
1258 12
    $stringy = static::create($this->str, $this->encoding);
1259
1260 12
    if ($stringy->startsWith($substring)) {
1261 6
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1262
1263 6
      return $stringy->substr($substringLength);
1264
    }
1265
1266 6
    return $stringy;
1267
  }
1268
1269
  /**
1270
   * Returns a new string with the suffix $substring removed, if present.
1271
   *
1272
   * @param  string $substring The suffix to remove
1273
   *
1274
   * @return Stringy Object having a $str without the suffix $substring
1275
   */
1276 12 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...
1277
  {
1278 12
    $stringy = static::create($this->str, $this->encoding);
1279
1280 12
    if ($stringy->endsWith($substring)) {
1281 8
      $substringLength = UTF8::strlen($substring, $stringy->encoding);
1282
1283 8
      return $stringy->substr(0, $stringy->length() - $substringLength);
1284
    }
1285
1286 4
    return $stringy;
1287
  }
1288
1289
  /**
1290
   * Returns a repeated string given a multiplier.
1291
   *
1292
   * @param  int $multiplier The number of times to repeat the string
1293
   *
1294
   * @return Stringy Object with a repeated str
1295
   */
1296 7
  public function repeat($multiplier)
1297
  {
1298 7
    $repeated = UTF8::str_repeat($this->str, $multiplier);
1299
1300 7
    return static::create($repeated, $this->encoding);
1301
  }
1302
1303
  /**
1304
   * Replaces all occurrences of $search in $str by $replacement.
1305
   *
1306
   * @param string $search        The needle to search for
1307
   * @param string $replacement   The string to replace with
1308
   * @param bool   $caseSensitive Whether or not to enforce case-sensitivity
1309
   *
1310
   * @return Stringy Object with the resulting $str after the replacements
1311
   */
1312 28 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...
1313
  {
1314 28
    if ($caseSensitive) {
1315 21
      $return = UTF8::str_replace($search, $replacement, $this->str);
1316
    } else {
1317 7
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1318
    }
1319
1320 28
    return static::create($return);
1321
  }
1322
1323
  /**
1324
   * Replaces all occurrences of $search in $str by $replacement.
1325
   *
1326
   * @param array        $search        The elements to search for
1327
   * @param string|array $replacement   The string to replace with
1328
   * @param bool         $caseSensitive Whether or not to enforce case-sensitivity
1329
   *
1330
   * @return Stringy Object with the resulting $str after the replacements
1331
   */
1332 30 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...
1333
  {
1334 30
    if ($caseSensitive) {
1335 23
      $return = UTF8::str_replace($search, $replacement, $this->str);
1336
    } else {
1337 7
      $return = UTF8::str_ireplace($search, $replacement, $this->str);
1338
    }
1339
1340 30
    return static::create($return);
1341
  }
1342
1343
  /**
1344
   * Replaces all occurrences of $search from the beginning of string with $replacement
1345
   *
1346
   * @param string $search
1347
   * @param string $replacement
1348
   *
1349
   * @return Stringy Object with the resulting $str after the replacements
1350
   */
1351 16
  public function replaceBeginning($search, $replacement)
1352
  {
1353 16
    $str = $this->regexReplace('^' . preg_quote($search, '/'), UTF8::str_replace('\\', '\\\\', $replacement));
1354
1355 16
    return static::create($str, $this->encoding);
1356
  }
1357
1358
  /**
1359
   * Replaces all occurrences of $search from the ending of string with $replacement
1360
   *
1361
   * @param string $search
1362
   * @param string $replacement
1363
   *
1364
   * @return Stringy Object with the resulting $str after the replacements
1365
   */
1366 16
  public function replaceEnding($search, $replacement)
1367
  {
1368 16
    $str = $this->regexReplace(preg_quote($search, '/') . '$', UTF8::str_replace('\\', '\\\\', $replacement));
1369
1370 16
    return static::create($str, $this->encoding);
1371
  }
1372
1373
  /**
1374
   * Returns a reversed string. A multibyte version of strrev().
1375
   *
1376
   * @return Stringy Object with a reversed $str
1377
   */
1378 5
  public function reverse()
1379
  {
1380 5
    $reversed = UTF8::strrev($this->str);
1381
1382 5
    return static::create($reversed, $this->encoding);
1383
  }
1384
1385
  /**
1386
   * Truncates the string to a given length, while ensuring that it does not
1387
   * split words. If $substring is provided, and truncating occurs, the
1388
   * string is further truncated so that the substring may be appended without
1389
   * exceeding the desired length.
1390
   *
1391
   * @param  int    $length    Desired length of the truncated string
1392
   * @param  string $substring The substring to append if it can fit
1393
   *
1394
   * @return Stringy Object with the resulting $str after truncating
1395
   */
1396 22
  public function safeTruncate($length, $substring = '')
1397
  {
1398 22
    $stringy = static::create($this->str, $this->encoding);
1399 22
    if ($length >= $stringy->length()) {
1400 4
      return $stringy;
1401
    }
1402
1403
    // Need to further trim the string so we can append the substring
1404 18
    $encoding = $stringy->encoding;
1405 18
    $substringLength = UTF8::strlen($substring, $encoding);
1406 18
    $length -= $substringLength;
1407
1408 18
    $truncated = UTF8::substr($stringy->str, 0, $length, $encoding);
1409
1410
    // If the last word was truncated
1411 18
    if (UTF8::strpos($stringy->str, ' ', $length - 1, $encoding) != $length) {
1412
      // Find pos of the last occurrence of a space, get up to that
1413 11
      $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 1408 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...
1414 11
      $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 1414 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 1413 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...
1415
    }
1416
1417 18
    $stringy->str = $truncated . $substring;
1418
1419 18
    return $stringy;
1420
  }
1421
1422
  /**
1423
   * A multibyte string shuffle function. It returns a string with its
1424
   * characters in random order.
1425
   *
1426
   * @return Stringy Object with a shuffled $str
1427
   */
1428 3
  public function shuffle()
1429
  {
1430 3
    $shuffledStr = UTF8::str_shuffle($this->str);
1431
1432 3
    return static::create($shuffledStr, $this->encoding);
1433
  }
1434
1435
  /**
1436
   * Converts the string into an URL slug. This includes replacing non-ASCII
1437
   * characters with their closest ASCII equivalents, removing remaining
1438
   * non-ASCII and non-alphanumeric characters, and replacing whitespace with
1439
   * $replacement. The replacement defaults to a single dash, and the string
1440
   * is also converted to lowercase.
1441
   *
1442
   * @param string $replacement The string used to replace whitespace
1443
   * @param string $language    The language for the url
1444
   * @param bool   $strToLower  string to lower
1445
   *
1446
   * @return Stringy Object whose $str has been converted to an URL slug
1447
   */
1448 15
  public function slugify($replacement = '-', $language = 'de', $strToLower = true)
1449
  {
1450 15
    $slug = URLify::slug($this->str, $language, $replacement, $strToLower);
1451
1452 15
    return static::create($slug, $this->encoding);
1453
  }
1454
1455
  /**
1456
   * Remove css media-queries.
1457
   *
1458
   * @return Stringy
1459
   */
1460 1
  public function stripeCssMediaQueries()
1461
  {
1462 1
    $pattern = '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU';
1463
1464 1
    return static::create(preg_replace($pattern, '', $this->str));
1465
  }
1466
1467
  /**
1468
   * Remove empty html-tag.
1469
   *
1470
   * e.g.: <tag></tag>
1471
   *
1472
   * @return Stringy
1473
   */
1474 1
  public function stripeEmptyHtmlTags()
1475
  {
1476 1
    $pattern = "/<[^\/>]*>(([\s]?)*|)<\/[^>]*>/i";
1477
1478 1
    return static::create(preg_replace($pattern, '', $this->str));
1479
  }
1480
1481
  /**
1482
   * Converts the string into an valid UTF-8 string.
1483
   *
1484
   * @return Stringy
1485
   */
1486 1
  public function utf8ify()
1487
  {
1488 1
    return static::create(UTF8::cleanup($this->str));
1489
  }
1490
1491
  /**
1492
   * escape html
1493
   *
1494
   * @return Stringy
1495
   */
1496 6
  public function escape()
1497
  {
1498 6
    $str = UTF8::htmlspecialchars(
1499 6
        $this->str,
1500 6
        ENT_QUOTES | ENT_SUBSTITUTE,
1501 6
        $this->encoding
1502
    );
1503
1504 6
    return static::create($str, $this->encoding);
1505
  }
1506
1507
  /**
1508
   * Create an extract from a text, so if the search-string was found, it will be centered in the output.
1509
   *
1510
   * @param string   $search
1511
   * @param int|null $length
1512
   * @param string   $ellipsis
1513
   *
1514
   * @return Stringy
1515
   */
1516 1
  public function extractText($search = '', $length = null, $ellipsis = '...')
1517
  {
1518
    // init
1519 1
    $text = $this->str;
1520
1521 1
    if (empty($text)) {
1522 1
      return static::create('', $this->encoding);
1523
    }
1524
1525 1
    $trimChars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1526
1527 1
    if ($length === null) {
1528 1
      $length = $this->length() / 2;
1529
    }
1530
1531 1
    if (empty($search)) {
1532
1533
      $stringLength = UTF8::strlen($text, $this->encoding);
1534
      $end = ($length - 1) > $stringLength ? $stringLength : ($length - 1);
1535
      $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...
1536
1537
      if ($pos) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pos of type false|integer is loosely compared to true; 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...
1538
        return static::create(
1539
            rtrim(
1540
                UTF8::substr($text, 0, $pos, $this->encoding),
1541
                $trimChars
1542
            ) . $ellipsis,
1543
            $this->encoding
1544
        );
1545
      } else {
1546
        return static::create($text, $this->encoding);
1547
      }
1548
1549
    }
1550
1551 1
    $wordPos = UTF8::strpos(
1552 1
        UTF8::strtolower($text),
1553 1
        UTF8::strtolower($search, $this->encoding),
1554 1
        null,
1555 1
        $this->encoding
1556
    );
1557 1
    $halfSide = (int)($wordPos - $length / 2 + UTF8::strlen($search, $this->encoding) / 2);
1558
1559 1
    if ($halfSide > 0) {
1560
1561 1
      $halfText = UTF8::substr($text, 0, $halfSide, $this->encoding);
1562 1
      $pos_start = max(UTF8::strrpos($halfText, ' ', 0), UTF8::strrpos($halfText, '.', 0));
0 ignored issues
show
Security Bug introduced by
It seems like $halfText defined by \voku\helper\UTF8::subst...fSide, $this->encoding) on line 1561 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...
1563
1564 1
      if (!$pos_start) {
1565 1
        $pos_start = 0;
1566
      }
1567
1568
    } else {
1569 1
      $pos_start = 0;
1570
    }
1571
1572 1
    if ($wordPos && $halfSide > 0) {
1573 1
      $l = $pos_start + $length - 1;
1574 1
      $realLength = UTF8::strlen($text, $this->encoding);
1575
1576 1
      if ($l > $realLength) {
1577
        $l = $realLength;
1578
      }
1579
1580 1
      $pos_end = min(
1581 1
                     UTF8::strpos($text, ' ', $l, $this->encoding),
1582 1
                     UTF8::strpos($text, '.', $l, $this->encoding)
1583 1
                 ) - $pos_start;
1584
1585 1
      if (!$pos_end || $pos_end <= 0) {
1586 1
        $extract = $ellipsis . ltrim(
1587 1
                UTF8::substr(
1588
                    $text,
1589
                    $pos_start,
1590 1
                    UTF8::strlen($text),
1591 1
                    $this->encoding
1592
                ),
1593
                $trimChars
1594
            );
1595 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...
1596 1
        $extract = $ellipsis . trim(
1597 1
                UTF8::substr(
1598
                    $text,
1599
                    $pos_start,
1600
                    $pos_end,
1601 1
                    $this->encoding
1602
                ),
1603
                $trimChars
1604 1
            ) . $ellipsis;
1605
      }
1606
1607
    } else {
1608
1609 1
      $l = $length - 1;
1610 1
      $trueLength = UTF8::strlen($text, $this->encoding);
1611
1612 1
      if ($l > $trueLength) {
1613
        $l = $trueLength;
1614
      }
1615
1616 1
      $pos_end = min(
1617 1
          UTF8::strpos($text, ' ', $l, $this->encoding),
1618 1
          UTF8::strpos($text, '.', $l, $this->encoding)
1619
      );
1620
1621 1 View Code Duplication
      if ($pos_end) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $pos_end of type false|integer is loosely compared to true; 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...
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...
1622 1
        $extract = rtrim(
1623 1
                       UTF8::substr($text, 0, $pos_end, $this->encoding),
1624
                       $trimChars
1625 1
                   ) . $ellipsis;
1626
      } else {
1627 1
        $extract = $text;
1628
      }
1629
    }
1630
1631 1
    return static::create($extract, $this->encoding);
1632
  }
1633
1634
1635
  /**
1636
   * remove xss from html
1637
   *
1638
   * @return Stringy
1639
   */
1640 6
  public function removeXss()
1641
  {
1642 6
    static $antiXss = null;
1643
1644 6
    if ($antiXss === null) {
1645 1
      $antiXss = new AntiXSS();
1646
    }
1647
1648 6
    $str = $antiXss->xss_clean($this->str);
1649
1650 6
    return static::create($str, $this->encoding);
1651
  }
1652
1653
  /**
1654
   * remove html-break [br | \r\n | \r | \n | ...]
1655
   *
1656
   * @param string $replacement
1657
   *
1658
   * @return Stringy
1659
   */
1660 6
  public function removeHtmlBreak($replacement = '')
1661
  {
1662 6
    $str = preg_replace('#/\r\n|\r|\n|<br.*/?>#isU', $replacement, $this->str);
1663
1664 6
    return static::create($str, $this->encoding);
1665
  }
1666
1667
  /**
1668
   * remove html
1669
   *
1670
   * @param $allowableTags
1671
   *
1672
   * @return Stringy
1673
   */
1674 6
  public function removeHtml($allowableTags = null)
1675
  {
1676 6
    $str = strip_tags($this->str, $allowableTags);
1677
1678 6
    return static::create($str, $this->encoding);
1679
  }
1680
1681
  /**
1682
   * Returns the substring beginning at $start, and up to, but not including
1683
   * the index specified by $end. If $end is omitted, the function extracts
1684
   * the remaining string. If $end is negative, it is computed from the end
1685
   * of the string.
1686
   *
1687
   * @param  int $start Initial index from which to begin extraction
1688
   * @param  int $end   Optional index at which to end extraction
1689
   *
1690
   * @return Stringy Object with its $str being the extracted substring
1691
   */
1692 18
  public function slice($start, $end = null)
1693
  {
1694 18
    if ($end === null) {
1695 4
      $length = $this->length();
1696 14
    } elseif ($end >= 0 && $end <= $start) {
1697 4
      return static::create('', $this->encoding);
1698 10
    } elseif ($end < 0) {
1699 2
      $length = $this->length() + $end - $start;
1700
    } else {
1701 8
      $length = $end - $start;
1702
    }
1703
1704 14
    $str = UTF8::substr($this->str, $start, $length, $this->encoding);
1705
1706 14
    return static::create($str, $this->encoding);
1707
  }
1708
1709
  /**
1710
   * Splits the string with the provided regular expression, returning an
1711
   * array of Stringy objects. An optional integer $limit will truncate the
1712
   * results.
1713
   *
1714
   * @param  string $pattern The regex with which to split the string
1715
   * @param  int    $limit   Optional maximum number of results to return
1716
   *
1717
   * @return Stringy[] An array of Stringy objects
1718
   */
1719 19
  public function split($pattern, $limit = null)
1720
  {
1721 19
    if ($limit === 0) {
1722 2
      return array();
1723
    }
1724
1725
    // UTF8::split errors when supplied an empty pattern in < PHP 5.4.13
1726
    // and current versions of HHVM (3.8 and below)
1727 17
    if ($pattern === '') {
1728 1
      return array(static::create($this->str, $this->encoding));
1729
    }
1730
1731
    // UTF8::split returns the remaining unsplit string in the last index when
1732
    // supplying a limit
1733 16
    if ($limit > 0) {
1734 8
      $limit += 1;
1735
    } else {
1736 8
      $limit = -1;
1737
    }
1738
1739 16
    $array = preg_split('/' . preg_quote($pattern, '/') . '/u', $this->str, $limit);
1740
1741 16
    if ($limit > 0 && count($array) === $limit) {
1742 4
      array_pop($array);
1743
    }
1744
1745
    /** @noinspection CallableInLoopTerminationConditionInspection */
1746
    /** @noinspection ForeachInvariantsInspection */
1747 16 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...
1748 16
      $array[$i] = static::create($array[$i], $this->encoding);
1749
    }
1750
1751 16
    return $array;
1752
  }
1753
1754
  /**
1755
   * Surrounds $str with the given substring.
1756
   *
1757
   * @param  string $substring The substring to add to both sides
1758
   *
1759
   * @return Stringy Object whose $str had the substring both prepended and
1760
   *                 appended
1761
   */
1762 5
  public function surround($substring)
1763
  {
1764 5
    $str = implode('', array($substring, $this->str, $substring));
1765
1766 5
    return static::create($str, $this->encoding);
1767
  }
1768
1769
  /**
1770
   * Returns a case swapped version of the string.
1771
   *
1772
   * @return Stringy Object whose $str has each character's case swapped
1773
   */
1774 5
  public function swapCase()
1775
  {
1776 5
    $stringy = static::create($this->str, $this->encoding);
1777
1778 5
    $stringy->str = UTF8::swapCase($stringy->str, $stringy->encoding);
1779
1780 5
    return $stringy;
1781
  }
1782
1783
  /**
1784
   * Returns a string with smart quotes, ellipsis characters, and dashes from
1785
   * Windows-1252 (commonly used in Word documents) replaced by their ASCII
1786
   * equivalents.
1787
   *
1788
   * @return Stringy Object whose $str has those characters removed
1789
   */
1790 4
  public function tidy()
1791
  {
1792 4
    $str = UTF8::normalize_msword($this->str);
1793
1794 4
    return static::create($str, $this->encoding);
1795
  }
1796
1797
  /**
1798
   * Returns a trimmed string with the first letter of each word capitalized.
1799
   * Also accepts an array, $ignore, allowing you to list words not to be
1800
   * capitalized.
1801
   *
1802
   * @param  array $ignore An array of words not to capitalize
1803
   *
1804
   * @return Stringy Object with a titleized $str
1805
   */
1806 5
  public function titleize($ignore = null)
1807
  {
1808 5
    $stringy = static::create($this->trim(), $this->encoding);
1809 5
    $encoding = $this->encoding;
1810
1811 5
    $stringy->str = preg_replace_callback(
1812 5
        '/([\S]+)/u',
1813
        function ($match) use ($encoding, $ignore) {
1814 5
          if ($ignore && in_array($match[0], $ignore, true)) {
1815 2
            return $match[0];
1816
          } else {
1817 5
            $stringy = new Stringy($match[0], $encoding);
1818
1819 5
            return (string)$stringy->toLowerCase()->upperCaseFirst();
1820
          }
1821 5
        },
1822 5
        $stringy->str
1823
    );
1824
1825 5
    return $stringy;
1826
  }
1827
1828
  /**
1829
   * Converts all characters in the string to lowercase. An alias for PHP's
1830
   * UTF8::strtolower().
1831
   *
1832
   * @return Stringy Object with all characters of $str being lowercase
1833
   */
1834 27
  public function toLowerCase()
1835
  {
1836 27
    $str = UTF8::strtolower($this->str, $this->encoding);
1837
1838 27
    return static::create($str, $this->encoding);
1839
  }
1840
1841
  /**
1842
   * Returns true if the string is base64 encoded, false otherwise.
1843
   *
1844
   * @return bool Whether or not $str is base64 encoded
1845
   */
1846 7
  public function isBase64()
1847
  {
1848 7
    return UTF8::is_base64($this->str);
1849
  }
1850
1851
  /**
1852
   * Returns an ASCII version of the string. A set of non-ASCII characters are
1853
   * replaced with their closest ASCII counterparts, and the rest are removed
1854
   * unless instructed otherwise.
1855
   *
1856
   * @param $strict [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad performance</p>
1857
   *
1858
   * @return Stringy Object whose $str contains only ASCII characters
1859
   */
1860 16
  public function toAscii($strict = false)
1861
  {
1862 16
    $str = UTF8::to_ascii($this->str, '?', $strict);
1863
1864 16
    return static::create($str, $this->encoding);
1865
  }
1866
1867
  /**
1868
   * Returns a boolean representation of the given logical string value.
1869
   * For example, 'true', '1', 'on' and 'yes' will return true. 'false', '0',
1870
   * 'off', and 'no' will return false. In all instances, case is ignored.
1871
   * For other numeric strings, their sign will determine the return value.
1872
   * In addition, blank strings consisting of only whitespace will return
1873
   * false. For all other strings, the return value is a result of a
1874
   * boolean cast.
1875
   *
1876
   * @return bool A boolean value for the string
1877
   */
1878 15
  public function toBoolean()
1879
  {
1880 15
    $key = $this->toLowerCase()->str;
1881
    $map = array(
1882 15
        'true'  => true,
1883
        '1'     => true,
1884
        'on'    => true,
1885
        'yes'   => true,
1886
        'false' => false,
1887
        '0'     => false,
1888
        'off'   => false,
1889
        'no'    => false,
1890
    );
1891
1892 15
    if (array_key_exists($key, $map)) {
1893 10
      return $map[$key];
1894 5
    } elseif (is_numeric($this->str)) {
1895 2
      return ((int)$this->str > 0);
1896
    } else {
1897 3
      return (bool)$this->regexReplace('[[:space:]]', '')->str;
1898
    }
1899
  }
1900
1901
  /**
1902
   * Return Stringy object as string, but you can also use (string) for automatically casting the object into a string.
1903
   *
1904
   * @return string
1905
   */
1906 967
  public function toString()
1907
  {
1908 967
    return (string)$this->str;
1909
  }
1910
1911
  /**
1912
   * Converts each tab in the string to some number of spaces, as defined by
1913
   * $tabLength. By default, each tab is converted to 4 consecutive spaces.
1914
   *
1915
   * @param  int $tabLength Number of spaces to replace each tab with
1916
   *
1917
   * @return Stringy Object whose $str has had tabs switched to spaces
1918
   */
1919 6 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...
1920
  {
1921 6
    $spaces = UTF8::str_repeat(' ', $tabLength);
1922 6
    $str = UTF8::str_replace("\t", $spaces, $this->str);
1923
1924 6
    return static::create($str, $this->encoding);
1925
  }
1926
1927
  /**
1928
   * Converts each occurrence of some consecutive number of spaces, as
1929
   * defined by $tabLength, to a tab. By default, each 4 consecutive spaces
1930
   * are converted to a tab.
1931
   *
1932
   * @param  int $tabLength Number of spaces to replace with a tab
1933
   *
1934
   * @return Stringy Object whose $str has had spaces switched to tabs
1935
   */
1936 5 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...
1937
  {
1938 5
    $spaces = UTF8::str_repeat(' ', $tabLength);
1939 5
    $str = UTF8::str_replace($spaces, "\t", $this->str);
1940
1941 5
    return static::create($str, $this->encoding);
1942
  }
1943
1944
  /**
1945
   * Converts the first character of each word in the string to uppercase.
1946
   *
1947
   * @return Stringy Object with all characters of $str being title-cased
1948
   */
1949 5
  public function toTitleCase()
1950
  {
1951
    // "mb_convert_case()" used a polyfill from the "UTF8"-Class
1952 5
    $str = mb_convert_case($this->str, MB_CASE_TITLE, $this->encoding);
1953
1954 5
    return static::create($str, $this->encoding);
1955
  }
1956
1957
  /**
1958
   * Converts all characters in the string to uppercase. An alias for PHP's
1959
   * UTF8::strtoupper().
1960
   *
1961
   * @return Stringy Object with all characters of $str being uppercase
1962
   */
1963 5
  public function toUpperCase()
1964
  {
1965 5
    $str = UTF8::strtoupper($this->str, $this->encoding);
1966
1967 5
    return static::create($str, $this->encoding);
1968
  }
1969
1970
  /**
1971
   * Returns a string with whitespace removed from the start of the string.
1972
   * Supports the removal of unicode whitespace. Accepts an optional
1973
   * string of characters to strip instead of the defaults.
1974
   *
1975
   * @param  string $chars Optional string of characters to strip
1976
   *
1977
   * @return Stringy Object with a trimmed $str
1978
   */
1979 13 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...
1980
  {
1981 13
    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...
1982 11
      $chars = '[:space:]';
1983
    } else {
1984 2
      $chars = preg_quote($chars, '/');
1985
    }
1986
1987 13
    return $this->regexReplace("^[$chars]+", '');
1988
  }
1989
1990
  /**
1991
   * Returns a string with whitespace removed from the end of the string.
1992
   * Supports the removal of unicode whitespace. Accepts an optional
1993
   * string of characters to strip instead of the defaults.
1994
   *
1995
   * @param  string $chars Optional string of characters to strip
1996
   *
1997
   * @return Stringy Object with a trimmed $str
1998
   */
1999 13 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...
2000
  {
2001 13
    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...
2002 11
      $chars = '[:space:]';
2003
    } else {
2004 2
      $chars = preg_quote($chars, '/');
2005
    }
2006
2007 13
    return $this->regexReplace("[$chars]+\$", '');
2008
  }
2009
2010
  /**
2011
   * Truncates the string to a given length. If $substring is provided, and
2012
   * truncating occurs, the string is further truncated so that the substring
2013
   * may be appended without exceeding the desired length.
2014
   *
2015
   * @param  int    $length    Desired length of the truncated string
2016
   * @param  string $substring The substring to append if it can fit
2017
   *
2018
   * @return Stringy Object with the resulting $str after truncating
2019
   */
2020 22 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...
2021
  {
2022 22
    $stringy = static::create($this->str, $this->encoding);
2023 22
    if ($length >= $stringy->length()) {
2024 4
      return $stringy;
2025
    }
2026
2027
    // Need to further trim the string so we can append the substring
2028 18
    $substringLength = UTF8::strlen($substring, $stringy->encoding);
2029 18
    $length -= $substringLength;
2030
2031 18
    $truncated = UTF8::substr($stringy->str, 0, $length, $stringy->encoding);
2032 18
    $stringy->str = $truncated . $substring;
2033
2034 18
    return $stringy;
2035
  }
2036
2037
  /**
2038
   * Returns a lowercase and trimmed string separated by underscores.
2039
   * Underscores are inserted before uppercase characters (with the exception
2040
   * of the first character of the string), and in place of spaces as well as
2041
   * dashes.
2042
   *
2043
   * @return Stringy Object with an underscored $str
2044
   */
2045 16
  public function underscored()
2046
  {
2047 16
    return $this->delimit('_');
2048
  }
2049
2050
  /**
2051
   * Returns an UpperCamelCase version of the supplied string. It trims
2052
   * surrounding spaces, capitalizes letters following digits, spaces, dashes
2053
   * and underscores, and removes spaces, dashes, underscores.
2054
   *
2055
   * @return Stringy Object with $str in UpperCamelCase
2056
   */
2057 13
  public function upperCamelize()
2058
  {
2059 13
    return $this->camelize()->upperCaseFirst();
2060
  }
2061
2062
  /**
2063
   * Returns a camelCase version of the string. Trims surrounding spaces,
2064
   * capitalizes letters following digits, spaces, dashes and underscores,
2065
   * and removes spaces, dashes, as well as underscores.
2066
   *
2067
   * @return Stringy Object with $str in camelCase
2068
   */
2069 32
  public function camelize()
2070
  {
2071 32
    $encoding = $this->encoding;
2072 32
    $stringy = $this->trim()->lowerCaseFirst();
2073 32
    $stringy->str = preg_replace('/^[-_]+/', '', $stringy->str);
2074
2075 32
    $stringy->str = preg_replace_callback(
2076 32
        '/[-_\s]+(.)?/u',
2077
        function ($match) use ($encoding) {
2078 27
          if (isset($match[1])) {
2079 27
            return UTF8::strtoupper($match[1], $encoding);
2080
          } else {
2081 1
            return '';
2082
          }
2083 32
        },
2084 32
        $stringy->str
2085
    );
2086
2087 32
    $stringy->str = preg_replace_callback(
2088 32
        '/[\d]+(.)?/u',
2089
        function ($match) use ($encoding) {
2090 6
          return UTF8::strtoupper($match[0], $encoding);
2091 32
        },
2092 32
        $stringy->str
2093
    );
2094
2095 32
    return $stringy;
2096
  }
2097
2098
  /**
2099
   * Convert a string to e.g.: "snake_case"
2100
   *
2101
   * @return Stringy Object with $str in snake_case
2102
   */
2103 20
  public function snakeize()
2104
  {
2105 20
    $str = $this->str;
2106
2107 20
    $encoding = $this->encoding;
2108 20
    $str = UTF8::normalize_whitespace($str);
2109 20
    $str = str_replace('-', '_', $str);
2110
2111 20
    $str = preg_replace_callback(
2112 20
        '/([\d|A-Z])/u',
2113 20
        function ($matches) use ($encoding) {
2114 8
          $match = $matches[1];
2115 8
          $matchInt = (int)$match;
2116
2117 8
          if ("$matchInt" == $match) {
2118 4
            return '_' . $match . '_';
2119
          } else {
2120 4
            return '_' . UTF8::strtolower($match, $encoding);
2121
          }
2122 20
        },
2123
        $str
2124
    );
2125
2126 20
    $str = preg_replace(
2127
        array(
2128
2129 20
            '/\s+/',      // convert spaces to "_"
2130
            '/^\s+|\s+$/',  // trim leading & trailing spaces
2131
            '/_+/',         // remove double "_"
2132
        ),
2133
        array(
2134 20
            '_',
2135
            '',
2136
            '_',
2137
        ),
2138
        $str
2139
    );
2140
2141 20
    $str = UTF8::trim($str, '_'); // trim leading & trailing "_"
2142 20
    $str = UTF8::trim($str); // trim leading & trailing whitespace
2143
2144 20
    return static::create($str, $this->encoding);
2145
  }
2146
2147
  /**
2148
   * Converts the first character of the string to lower case.
2149
   *
2150
   * @return Stringy Object with the first character of $str being lower case
2151
   */
2152 37 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...
2153
  {
2154 37
    $first = UTF8::substr($this->str, 0, 1, $this->encoding);
2155 37
    $rest = UTF8::substr($this->str, 1, $this->length() - 1, $this->encoding);
2156
2157 37
    $str = UTF8::strtolower($first, $this->encoding) . $rest;
0 ignored issues
show
Security Bug introduced by
It seems like $first defined by \voku\helper\UTF8::subst... 0, 1, $this->encoding) on line 2154 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...
2158
2159 37
    return static::create($str, $this->encoding);
2160
  }
2161
2162
  /**
2163
   * Shorten the string after $length, but also after the next word.
2164
   *
2165
   * @param int    $length
2166
   * @param string $strAddOn
2167
   *
2168
   * @return Stringy
2169
   */
2170 4
  public function shortenAfterWord($length, $strAddOn = '...')
2171
  {
2172 4
    $string = UTF8::str_limit_after_word($this->str, $length, $strAddOn);
2173
2174 4
    return static::create($string);
2175
  }
2176
2177
  /**
2178
   * Line-Wrap the string after $limit, but also after the next word.
2179
   *
2180
   * @param int $limit
2181
   *
2182
   * @return Stringy
2183
   */
2184 1
  public function lineWrapAfterWord($limit)
2185
  {
2186 1
    $strings = preg_split('/\\r\\n|\\r|\\n/', $this->str);
2187
2188 1
    $string = '';
2189 1
    foreach ($strings as $value) {
2190 1
      $string .= wordwrap($value, $limit);
2191 1
      $string .= "\n";
2192
    }
2193
2194 1
    return static::create($string);
2195
  }
2196
2197
  /**
2198
   * Gets the substring after the first occurrence of a separator.
2199
   * If no match is found returns new empty Stringy object.
2200
   *
2201
   * @param string $separator
2202
   *
2203
   * @return Stringy
2204
   */
2205 1 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...
2206
  {
2207 1
    if (($offset = $this->indexOf($separator)) === false) {
2208 1
      return static::create('');
2209
    }
2210
2211 1
    return static::create(
2212 1
        UTF8::substr(
2213 1
            $this->str,
2214 1
            $offset + UTF8::strlen($separator, $this->encoding),
2215 1
            null,
2216 1
            $this->encoding
2217
        ),
2218 1
        $this->encoding
2219
    );
2220
  }
2221
2222
  /**
2223
   * Gets the substring after the last occurrence of a separator.
2224
   * If no match is found returns new empty Stringy object.
2225
   *
2226
   * @param string $separator
2227
   *
2228
   * @return Stringy
2229
   */
2230 1 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...
2231
  {
2232 1
    $offset = $this->indexOfLast($separator);
2233 1
    if ($offset === false) {
2234 1
      return static::create('');
2235
    }
2236
2237 1
    return static::create(
2238 1
        UTF8::substr(
2239 1
            $this->str,
2240 1
            $offset + UTF8::strlen($separator, $this->encoding),
2241 1
            null,
2242 1
            $this->encoding
2243
        ),
2244 1
        $this->encoding
2245
    );
2246
  }
2247
2248
  /**
2249
   * Gets the substring before the first occurrence of a separator.
2250
   * If no match is found returns new empty Stringy object.
2251
   *
2252
   * @param string $separator
2253
   *
2254
   * @return Stringy
2255
   */
2256 1 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...
2257
  {
2258 1
    $offset = $this->indexOf($separator);
2259 1
    if ($offset === false) {
2260 1
      return static::create('');
2261
    }
2262
2263 1
    return static::create(
2264 1
        UTF8::substr(
2265 1
            $this->str,
2266 1
            0,
2267
            $offset,
2268 1
            $this->encoding
2269
        ),
2270 1
        $this->encoding
2271
    );
2272
  }
2273
2274
  /**
2275
   * Gets the substring before the last occurrence of a separator.
2276
   * If no match is found returns new empty Stringy object.
2277
   *
2278
   * @param string $separator
2279
   *
2280
   * @return Stringy
2281
   */
2282 1 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...
2283
  {
2284 1
    $offset = $this->indexOfLast($separator);
2285 1
    if ($offset === false) {
2286 1
      return static::create('');
2287
    }
2288
2289 1
    return static::create(
2290 1
        UTF8::substr(
2291 1
            $this->str,
2292 1
            0,
2293
            $offset,
2294 1
            $this->encoding
2295
        ),
2296 1
        $this->encoding
2297
    );
2298
  }
2299
}
2300