AbstractString   F
last analyzed

Complexity

Total Complexity 125

Size/Duplication

Total Lines 696
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 125
eloc 229
c 1
b 0
f 0
dl 0
loc 696
rs 2

30 Methods

Rating   Name   Duplication   Size   Complexity  
B toByteString() 0 29 8
A containsAny() 0 3 1
A collapseWhitespace() 0 6 1
A bytesAt() 0 5 2
B indexOf() 0 17 7
A toUnicodeString() 0 3 1
A __toString() 0 3 1
A jsonSerialize() 0 3 1
A isEmpty() 0 3 1
A endsWith() 0 13 5
A indexOfLast() 0 17 6
B split() 0 43 10
A startsWith() 0 13 5
A ignoreCase() 0 6 1
B truncate() 0 25 8
A unwrap() 0 11 5
A before() 0 24 6
A after() 0 24 6
A equalsTo() 0 13 5
A ensureEnd() 0 10 3
A repeat() 0 10 2
A beforeLast() 0 24 6
C wordwrap() 0 44 12
A ensureStart() 0 17 3
A toCodePointString() 0 3 1
B wrap() 0 21 9
A __clone() 0 3 1
A toString() 0 3 1
A afterLast() 0 24 6
A __sleep() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractString often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractString, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\String;
13
14
use Symfony\Component\String\Exception\ExceptionInterface;
15
use Symfony\Component\String\Exception\InvalidArgumentException;
16
use Symfony\Component\String\Exception\RuntimeException;
17
18
/**
19
 * Represents a string of abstract characters.
20
 *
21
 * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters).
22
 * This class is the abstract type to use as a type-hint when the logic you want to
23
 * implement doesn't care about the exact variant it deals with.
24
 *
25
 * @author Nicolas Grekas <[email protected]>
26
 * @author Hugo Hamon <[email protected]>
27
 *
28
 * @throws ExceptionInterface
29
 */
30
abstract class AbstractString implements \Stringable, \JsonSerializable
31
{
32
    public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER;
33
    public const PREG_SET_ORDER = \PREG_SET_ORDER;
34
    public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE;
35
    public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL;
36
37
    public const PREG_SPLIT = 0;
38
    public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY;
39
    public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE;
40
    public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE;
41
42
    protected $string = '';
43
    protected $ignoreCase = false;
44
45
    abstract public function __construct(string $string = '');
46
47
    /**
48
     * Unwraps instances of AbstractString back to strings.
49
     *
50
     * @return string[]|array
51
     */
52
    public static function unwrap(array $values): array
53
    {
54
        foreach ($values as $k => $v) {
55
            if ($v instanceof self) {
56
                $values[$k] = $v->__toString();
57
            } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) {
58
                $values[$k] = $v;
59
            }
60
        }
61
62
        return $values;
63
    }
64
65
    /**
66
     * Wraps (and normalizes) strings in instances of AbstractString.
67
     *
68
     * @return static[]|array
69
     */
70
    public static function wrap(array $values): array
71
    {
72
        $i = 0;
73
        $keys = null;
74
75
        foreach ($values as $k => $v) {
76
            if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) {
77
                $keys = $keys ?? array_keys($values);
78
                $keys[$i] = $j;
79
            }
80
81
            if (\is_string($v)) {
82
                $values[$k] = new static($v);
83
            } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) {
84
                $values[$k] = $v;
85
            }
86
87
            ++$i;
88
        }
89
90
        return null !== $keys ? array_combine($keys, $values) : $values;
91
    }
92
93
    /**
94
     * @param string|string[] $needle
95
     *
96
     * @return static
97
     */
98
    public function after($needle, bool $includeNeedle = false, int $offset = 0): self
99
    {
100
        $str = clone $this;
101
        $i = \PHP_INT_MAX;
102
103
        foreach ((array) $needle as $n) {
104
            $n = (string) $n;
105
            $j = $this->indexOf($n, $offset);
106
107
            if (null !== $j && $j < $i) {
108
                $i = $j;
109
                $str->string = $n;
110
            }
111
        }
112
113
        if (\PHP_INT_MAX === $i) {
114
            return $str;
115
        }
116
117
        if (!$includeNeedle) {
118
            $i += $str->length();
119
        }
120
121
        return $this->slice($i);
122
    }
123
124
    /**
125
     * @param string|string[] $needle
126
     *
127
     * @return static
128
     */
129
    public function afterLast($needle, bool $includeNeedle = false, int $offset = 0): self
130
    {
131
        $str = clone $this;
132
        $i = null;
133
134
        foreach ((array) $needle as $n) {
135
            $n = (string) $n;
136
            $j = $this->indexOfLast($n, $offset);
137
138
            if (null !== $j && $j >= $i) {
139
                $i = $offset = $j;
140
                $str->string = $n;
141
            }
142
        }
143
144
        if (null === $i) {
145
            return $str;
146
        }
147
148
        if (!$includeNeedle) {
149
            $i += $str->length();
150
        }
151
152
        return $this->slice($i);
153
    }
154
155
    /**
156
     * @return static
157
     */
158
    abstract public function append(string ...$suffix): self;
159
160
    /**
161
     * @param string|string[] $needle
162
     *
163
     * @return static
164
     */
165
    public function before($needle, bool $includeNeedle = false, int $offset = 0): self
166
    {
167
        $str = clone $this;
168
        $i = \PHP_INT_MAX;
169
170
        foreach ((array) $needle as $n) {
171
            $n = (string) $n;
172
            $j = $this->indexOf($n, $offset);
173
174
            if (null !== $j && $j < $i) {
175
                $i = $j;
176
                $str->string = $n;
177
            }
178
        }
179
180
        if (\PHP_INT_MAX === $i) {
181
            return $str;
182
        }
183
184
        if ($includeNeedle) {
185
            $i += $str->length();
186
        }
187
188
        return $this->slice(0, $i);
189
    }
190
191
    /**
192
     * @param string|string[] $needle
193
     *
194
     * @return static
195
     */
196
    public function beforeLast($needle, bool $includeNeedle = false, int $offset = 0): self
197
    {
198
        $str = clone $this;
199
        $i = null;
200
201
        foreach ((array) $needle as $n) {
202
            $n = (string) $n;
203
            $j = $this->indexOfLast($n, $offset);
204
205
            if (null !== $j && $j >= $i) {
206
                $i = $offset = $j;
207
                $str->string = $n;
208
            }
209
        }
210
211
        if (null === $i) {
212
            return $str;
213
        }
214
215
        if ($includeNeedle) {
216
            $i += $str->length();
217
        }
218
219
        return $this->slice(0, $i);
220
    }
221
222
    /**
223
     * @return int[]
224
     */
225
    public function bytesAt(int $offset): array
226
    {
227
        $str = $this->slice($offset, 1);
228
229
        return '' === $str->string ? [] : array_values(unpack('C*', $str->string));
230
    }
231
232
    /**
233
     * @return static
234
     */
235
    abstract public function camel(): self;
236
237
    /**
238
     * @return static[]
239
     */
240
    abstract public function chunk(int $length = 1): array;
241
242
    /**
243
     * @return static
244
     */
245
    public function collapseWhitespace(): self
246
    {
247
        $str = clone $this;
248
        $str->string = trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $str->string));
249
250
        return $str;
251
    }
252
253
    /**
254
     * @param string|string[] $needle
255
     */
256
    public function containsAny($needle): bool
257
    {
258
        return null !== $this->indexOf($needle);
259
    }
260
261
    /**
262
     * @param string|string[] $suffix
263
     */
264
    public function endsWith($suffix): bool
265
    {
266
        if (!\is_array($suffix) && !$suffix instanceof \Traversable) {
267
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
268
        }
269
270
        foreach ($suffix as $s) {
271
            if ($this->endsWith((string) $s)) {
272
                return true;
273
            }
274
        }
275
276
        return false;
277
    }
278
279
    /**
280
     * @return static
281
     */
282
    public function ensureEnd(string $suffix): self
283
    {
284
        if (!$this->endsWith($suffix)) {
285
            return $this->append($suffix);
286
        }
287
288
        $suffix = preg_quote($suffix);
289
        $regex = '{('.$suffix.')(?:'.$suffix.')++$}D';
290
291
        return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1');
292
    }
293
294
    /**
295
     * @return static
296
     */
297
    public function ensureStart(string $prefix): self
298
    {
299
        $prefix = new static($prefix);
300
301
        if (!$this->startsWith($prefix)) {
302
            return $this->prepend($prefix);
0 ignored issues
show
Bug introduced by
It seems like $prefix can also be of type array; however, parameter $prefix of Symfony\Component\String\AbstractString::prepend() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

302
            return $this->prepend(/** @scrutinizer ignore-type */ $prefix);
Loading history...
303
        }
304
305
        $str = clone $this;
306
        $i = $prefixLen = $prefix->length();
307
308
        while ($this->indexOf($prefix, $i) === $i) {
309
            $str = $str->slice($prefixLen);
310
            $i += $prefixLen;
311
        }
312
313
        return $str;
314
    }
315
316
    /**
317
     * @param string|string[] $string
318
     */
319
    public function equalsTo($string): bool
320
    {
321
        if (!\is_array($string) && !$string instanceof \Traversable) {
322
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
323
        }
324
325
        foreach ($string as $s) {
326
            if ($this->equalsTo((string) $s)) {
327
                return true;
328
            }
329
        }
330
331
        return false;
332
    }
333
334
    /**
335
     * @return static
336
     */
337
    abstract public function folded(): self;
338
339
    /**
340
     * @return static
341
     */
342
    public function ignoreCase(): self
343
    {
344
        $str = clone $this;
345
        $str->ignoreCase = true;
346
347
        return $str;
348
    }
349
350
    /**
351
     * @param string|string[] $needle
352
     */
353
    public function indexOf($needle, int $offset = 0): ?int
354
    {
355
        if (!\is_array($needle) && !$needle instanceof \Traversable) {
356
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
357
        }
358
359
        $i = \PHP_INT_MAX;
360
361
        foreach ($needle as $n) {
362
            $j = $this->indexOf((string) $n, $offset);
363
364
            if (null !== $j && $j < $i) {
365
                $i = $j;
366
            }
367
        }
368
369
        return \PHP_INT_MAX === $i ? null : $i;
370
    }
371
372
    /**
373
     * @param string|string[] $needle
374
     */
375
    public function indexOfLast($needle, int $offset = 0): ?int
376
    {
377
        if (!\is_array($needle) && !$needle instanceof \Traversable) {
378
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
379
        }
380
381
        $i = null;
382
383
        foreach ($needle as $n) {
384
            $j = $this->indexOfLast((string) $n, $offset);
385
386
            if (null !== $j && $j >= $i) {
387
                $i = $offset = $j;
388
            }
389
        }
390
391
        return $i;
392
    }
393
394
    public function isEmpty(): bool
395
    {
396
        return '' === $this->string;
397
    }
398
399
    /**
400
     * @return static
401
     */
402
    abstract public function join(array $strings, string $lastGlue = null): self;
403
404
    public function jsonSerialize(): string
405
    {
406
        return $this->string;
407
    }
408
409
    abstract public function length(): int;
410
411
    /**
412
     * @return static
413
     */
414
    abstract public function lower(): self;
415
416
    /**
417
     * Matches the string using a regular expression.
418
     *
419
     * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression.
420
     *
421
     * @return array All matches in a multi-dimensional array ordered according to flags
422
     */
423
    abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array;
424
425
    /**
426
     * @return static
427
     */
428
    abstract public function padBoth(int $length, string $padStr = ' '): self;
429
430
    /**
431
     * @return static
432
     */
433
    abstract public function padEnd(int $length, string $padStr = ' '): self;
434
435
    /**
436
     * @return static
437
     */
438
    abstract public function padStart(int $length, string $padStr = ' '): self;
439
440
    /**
441
     * @return static
442
     */
443
    abstract public function prepend(string ...$prefix): self;
444
445
    /**
446
     * @return static
447
     */
448
    public function repeat(int $multiplier): self
449
    {
450
        if (0 > $multiplier) {
451
            throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier));
452
        }
453
454
        $str = clone $this;
455
        $str->string = str_repeat($str->string, $multiplier);
456
457
        return $str;
458
    }
459
460
    /**
461
     * @return static
462
     */
463
    abstract public function replace(string $from, string $to): self;
464
465
    /**
466
     * @param string|callable $to
467
     *
468
     * @return static
469
     */
470
    abstract public function replaceMatches(string $fromRegexp, $to): self;
471
472
    /**
473
     * @return static
474
     */
475
    abstract public function reverse(): self;
476
477
    /**
478
     * @return static
479
     */
480
    abstract public function slice(int $start = 0, int $length = null): self;
481
482
    /**
483
     * @return static
484
     */
485
    abstract public function snake(): self;
486
487
    /**
488
     * @return static
489
     */
490
    abstract public function splice(string $replacement, int $start = 0, int $length = null): self;
491
492
    /**
493
     * @return static[]
494
     */
495
    public function split(string $delimiter, int $limit = null, int $flags = null): array
496
    {
497
        if (null === $flags) {
498
            throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.');
499
        }
500
501
        if ($this->ignoreCase) {
502
            $delimiter .= 'i';
503
        }
504
505
        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
506
507
        try {
508
            if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) {
509
                $lastError = preg_last_error();
510
511
                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
512
                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
513
                        throw new RuntimeException('Splitting failed with '.$k.'.');
514
                    }
515
                }
516
517
                throw new RuntimeException('Splitting failed with unknown error code.');
518
            }
519
        } finally {
520
            restore_error_handler();
521
        }
522
523
        $str = clone $this;
524
525
        if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) {
526
            foreach ($chunks as &$chunk) {
527
                $str->string = $chunk[0];
528
                $chunk[0] = clone $str;
529
            }
530
        } else {
531
            foreach ($chunks as &$chunk) {
532
                $str->string = $chunk;
533
                $chunk = clone $str;
534
            }
535
        }
536
537
        return $chunks;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $chunks returns the type array<mixed,array|string> which is incompatible with the documented return type array<mixed,Symfony\Comp...\String\AbstractString>.
Loading history...
538
    }
539
540
    /**
541
     * @param string|string[] $prefix
542
     */
543
    public function startsWith($prefix): bool
544
    {
545
        if (!\is_array($prefix) && !$prefix instanceof \Traversable) {
546
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
547
        }
548
549
        foreach ($prefix as $prefix) {
0 ignored issues
show
introduced by
$prefix is overwriting one of the parameters of this function.
Loading history...
550
            if ($this->startsWith((string) $prefix)) {
551
                return true;
552
            }
553
        }
554
555
        return false;
556
    }
557
558
    /**
559
     * @return static
560
     */
561
    abstract public function title(bool $allWords = false): self;
562
563
    public function toByteString(string $toEncoding = null): ByteString
564
    {
565
        $b = new ByteString();
566
567
        $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding;
568
569
        if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') {
0 ignored issues
show
Unused Code introduced by
The assignment to $fromEncoding is dead and can be removed.
Loading history...
570
            $b->string = $this->string;
571
572
            return $b;
573
        }
574
575
        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
576
577
        try {
578
            try {
579
                $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
580
            } catch (InvalidArgumentException $e) {
581
                if (!\function_exists('iconv')) {
582
                    throw $e;
583
                }
584
585
                $b->string = iconv('UTF-8', $toEncoding, $this->string);
586
            }
587
        } finally {
588
            restore_error_handler();
589
        }
590
591
        return $b;
592
    }
593
594
    public function toCodePointString(): CodePointString
595
    {
596
        return new CodePointString($this->string);
597
    }
598
599
    public function toString(): string
600
    {
601
        return $this->string;
602
    }
603
604
    public function toUnicodeString(): UnicodeString
605
    {
606
        return new UnicodeString($this->string);
607
    }
608
609
    /**
610
     * @return static
611
     */
612
    abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;
613
614
    /**
615
     * @return static
616
     */
617
    abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;
618
619
    /**
620
     * @return static
621
     */
622
    abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;
623
624
    /**
625
     * @return static
626
     */
627
    public function truncate(int $length, string $ellipsis = '', bool $cut = true): self
628
    {
629
        $stringLength = $this->length();
630
631
        if ($stringLength <= $length) {
632
            return clone $this;
633
        }
634
635
        $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0;
636
637
        if ($length < $ellipsisLength) {
638
            $ellipsisLength = 0;
639
        }
640
641
        if (!$cut) {
642
            if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
643
                return clone $this;
644
            }
645
646
            $length += $ellipsisLength;
647
        }
648
649
        $str = $this->slice(0, $length - $ellipsisLength);
650
651
        return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str;
652
    }
653
654
    /**
655
     * @return static
656
     */
657
    abstract public function upper(): self;
658
659
    /**
660
     * Returns the printable length on a terminal.
661
     */
662
    abstract public function width(bool $ignoreAnsiDecoration = true): int;
663
664
    /**
665
     * @return static
666
     */
667
    public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): self
668
    {
669
        $lines = '' !== $break ? $this->split($break) : [clone $this];
670
        $chars = [];
671
        $mask = '';
672
673
        if (1 === \count($lines) && '' === $lines[0]->string) {
674
            return $lines[0];
675
        }
676
677
        foreach ($lines as $i => $line) {
678
            if ($i) {
679
                $chars[] = $break;
680
                $mask .= '#';
681
            }
682
683
            foreach ($line->chunk() as $char) {
684
                $chars[] = $char->string;
685
                $mask .= ' ' === $char->string ? ' ' : '?';
686
            }
687
        }
688
689
        $string = '';
690
        $j = 0;
691
        $b = $i = -1;
692
        $mask = wordwrap($mask, $width, '#', $cut);
693
694
        while (false !== $b = strpos($mask, '#', $b + 1)) {
695
            for (++$i; $i < $b; ++$i) {
696
                $string .= $chars[$j];
697
                unset($chars[$j++]);
698
            }
699
700
            if ($break === $chars[$j] || ' ' === $chars[$j]) {
701
                unset($chars[$j++]);
702
            }
703
704
            $string .= $break;
705
        }
706
707
        $str = clone $this;
708
        $str->string = $string.implode('', $chars);
709
710
        return $str;
711
    }
712
713
    public function __sleep(): array
714
    {
715
        return ['string'];
716
    }
717
718
    public function __clone()
719
    {
720
        $this->ignoreCase = false;
721
    }
722
723
    public function __toString(): string
724
    {
725
        return $this->string;
726
    }
727
}
728