Text   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 482
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 87
dl 0
loc 482
ccs 107
cts 107
cp 1
rs 8.8
c 1
b 0
f 0
wmc 45

29 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 2 1
A __construct() 0 3 1
A padEnd() 0 2 1
A wrapWords() 0 2 1
A countSubstring() 0 12 3
A splice() 0 8 1
A replace() 0 7 5
A getString() 0 2 1
A pad() 0 4 1
A append() 0 2 1
A applyPadding() 0 12 3
A slice() 0 5 1
A toString() 0 2 1
A supplant() 0 2 1
A length() 0 2 1
A __toString() 0 2 1
A substring() 0 17 3
A ensureEnd() 0 6 2
A trimEnd() 0 2 1
A truncate() 0 12 3
A repeat() 0 2 1
A padStart() 0 2 1
A ensureStart() 0 6 2
A trim() 0 2 1
A insert() 0 13 3
A trimStart() 0 2 1
A reverse() 0 2 1
A getEncoding() 0 2 1
A prepend() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Text 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 Text, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
/**
3
 * This file is part of the Phootwork package.
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 *
7
 * @license MIT License
8
 * @copyright Thomas Gossmann
9
 */
10
namespace phootwork\lang;
11
12
use phootwork\lang\parts\ArrayConversionsPart;
13
use phootwork\lang\parts\CheckerPart;
14
use phootwork\lang\parts\ComparisonPart;
15
use phootwork\lang\parts\InternalPart;
16
use phootwork\lang\parts\SearchPart;
17
use phootwork\lang\parts\TransformationsPart;
18
use Stringable;
19
20
/**
21
 * Object representation of an immutable String
22
 *
23
 * @author gossi
24
 */
25
class Text implements Comparable, Stringable {
26
	use ArrayConversionsPart;
27
	use CheckerPart;
28
	use ComparisonPart;
29
	use SearchPart;
30
	use InternalPart;
31
	use TransformationsPart;
32
33
	/** @var string */
34
	private string $string;
35
36
	/** @var string */
37
	private string $encoding;
38
39
	/**
40
	 * Initializes a String object ad assigns both string and encoding properties
41
	 * the supplied values. $string is cast to a string prior to assignment, and if
42
	 * $encoding is not specified, it defaults to mb_internal_encoding(). Throws
43
	 * an InvalidArgumentException if the first argument is an array or object
44
	 * without a __toString method.
45
	 *
46
	 * @param string|Stringable $string   Value to modify, after being cast to string
47
	 * @param string|null $encoding The character encoding
48
	 *
49
	 * @psalm-suppress PossiblyInvalidPropertyAssignmentValue mb_internal_encoding always return string when called as getter
50
	 */
51 116
	public function __construct(string|Stringable $string = '', ?string $encoding = null) {
52 116
		$this->string = (string) $string;
53 116
		$this->encoding = $encoding ?? mb_internal_encoding();
0 ignored issues
show
Documentation Bug introduced by
It seems like $encoding ?? mb_internal_encoding() can also be of type true. However, the property $encoding 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...
54
	}
55
56
	/**
57
	 * Static initializing a String object.
58
	 *
59
	 * @param string|Stringable       $string
60
	 * @param string|null $encoding
61
	 *
62
	 * @return static
63
	 *
64
	 * @see Text::__construct()
65
	 *
66
	 * @psalm-suppress UnsafeInstantiation
67
	 */
68 10
	public static function create(string|Stringable $string, ?string $encoding = null): static {
69 10
		return new static($string, $encoding);
70
	}
71
72
	/**
73
	 * Returns the used encoding
74
	 *
75
	 * @return string
76
	 */
77 1
	public function getEncoding(): string {
78 1
		return $this->encoding;
79
	}
80
81
	/**
82
	 * Get string length
83
	 *
84
	 * <code>
85
	 * $str = new Text('Hello World!');<br>
86
	 * $str->length(); // 12
87
	 *
88
	 * $str = new Text('いちりんしゃ');<br>
89
	 * $str->length(); // 6
90
	 * </code>
91
	 *
92
	 * @return int Returns the length
93
	 */
94 30
	public function length(): int {
95 30
		return mb_strlen($this->string, $this->encoding);
96
	}
97
98
	/**
99
	 * Appends <code>$string</code> and returns as a new <code>Text</code>
100
	 *
101
	 * @param string|Stringable $string
102
	 *
103
	 * @return Text
104
	 */
105 10
	public function append(string|Stringable $string): self {
106 10
		return new self($this->string . $string, $this->encoding);
107
	}
108
109
	/**
110
	 * Prepends <code>$string</code> and returns as a new <code>Text</code>
111
	 *
112
	 * @param string|Stringable $string $string
113
	 *
114
	 * @return Text
115
	 */
116 3
	public function prepend(string|Stringable $string): self {
117 3
		return new self($string . $this->string, $this->encoding);
118
	}
119
120
	/**
121
	 * Inserts a substring at the given index
122
	 *
123
	 * <code>
124
	 * $str = new Text('Hello World!');<br>
125
	 * $str->insert('to this ', 5); // Hello to this World!
126
	 * </code>
127
	 *
128
	 * @param string|Stringable $substring
129
	 * @param int               $index
130
	 *
131
	 * @return Text
132
	 */
133 1
	public function insert(string|Stringable $substring, int $index): self {
134 1
		if ($index <= 0) {
135 1
			return $this->prepend($substring);
136
		}
137
138 1
		if ($index > $this->length()) {
139 1
			return $this->append($substring);
140
		}
141
142 1
		$start = mb_substr($this->string, 0, $index, $this->encoding);
143 1
		$end = mb_substr($this->string, $index, $this->length(), $this->encoding);
144
145 1
		return new self($start . $substring . $end);
146
	}
147
148
	//
149
	//
150
	// SLICING AND SUBSTRING
151
	//
152
	//
153
154
	/**
155
	 * Slices a piece of the string from a given offset with a specified length.
156
	 * If no length is given, the String is sliced to its maximum length.
157
	 *
158
	 * @see #substring
159
	 *
160
	 * @param int      $offset
161
	 * @param int|null $length
162
	 *
163
	 * @return Text
164
	 */
165 11
	public function slice(int $offset, ?int $length = null): self {
166 11
		$offset = $this->prepareOffset($offset);
167 11
		$length = $this->prepareLength($offset, $length);
168
169 11
		return new self(mb_substr($this->string, $offset, $length, $this->encoding), $this->encoding);
170
	}
171
172
	/**
173
	 * Slices a piece of the string from a given start to an end.
174
	 * If no length is given, the String is sliced to its maximum length.
175
	 *
176
	 * @see #slice
177
	 *
178
	 * @param int      $start
179
	 * @param int|null $end
180
	 *
181
	 * @return Text
182
	 */
183 20
	public function substring(int $start, ?int $end = null): self {
184 20
		$length = $this->length();
185
186 20
		if (null === $end) {
187 18
			$end = $length;
188
		}
189
190 20
		if ($end < 0) {
191 2
			$end = $length + $end;
192
		}
193
194 20
		$end = min($end, $length);
195 20
		$start = min($start, $end);
196 20
		$end = max($start, $end);
197 20
		$end = $end - $start;
198
199 20
		return new self(mb_substr($this->string, $start, $end, $this->encoding), $this->encoding);
200
	}
201
202
	/**
203
	 * Count the number of substring occurrences.
204
	 *
205
	 * @param string|Stringable $substring     The substring to count the occurrencies
206
	 * @param bool              $caseSensitive Force case-sensitivity
207
	 *
208
	 * @return int
209
	 */
210 3
	public function countSubstring(string|Stringable $substring, bool $caseSensitive = true): int {
211 3
		if (empty($substring)) {
212 1
			throw new \InvalidArgumentException('$substring cannot be empty');
213
		}
214
215 2
		if ($caseSensitive) {
216 2
			return mb_substr_count($this->string, (string) $substring, $this->encoding);
217
		}
218 1
		$str = mb_strtoupper($this->string, $this->encoding);
219 1
		$substring = mb_strtoupper((string) $substring, $this->encoding);
220
221 1
		return mb_substr_count($str, $substring, $this->encoding);
222
	}
223
224
	//
225
	//
226
	// REPLACING
227
	//
228
	//
229
230
	/**
231
	 * Replace all occurrences of the search string with the replacement string
232
	 *
233
	 * @see #supplant
234
	 *
235
	 * @param Arrayable|Stringable|array|string $search
236
	 * 		The value being searched for, otherwise known as the needle. An array may be used
237
	 * 		to designate multiple needles.
238
	 * @param Arrayable|Stringable[]|array|string $replace
239
	 * 		The replacement value that replaces found search values. An array may be used to
240
	 * 		designate multiple replacements.
241
	 *
242
	 * @return Text
243
	 *
244
	 * @psalm-suppress MixedArgumentTypeCoercion
245
	 */
246 8
	public function replace(Arrayable|Stringable|array|string $search, Arrayable|Stringable|array|string $replace): self {
247 8
		$search = $search instanceof Stringable ? (string) $search :
0 ignored issues
show
introduced by
$search is never a sub-type of Stringable.
Loading history...
248 6
			($search instanceof Arrayable ? $search->toArray() : $search);
0 ignored issues
show
introduced by
$search is never a sub-type of phootwork\lang\Arrayable.
Loading history...
249 8
		$replace = $replace instanceof Stringable ? (string) $replace :
0 ignored issues
show
introduced by
$replace is never a sub-type of Stringable.
Loading history...
250 8
			($replace instanceof Arrayable ? $replace->toArray() : $replace);
0 ignored issues
show
introduced by
$replace is never a sub-type of phootwork\lang\Arrayable.
Loading history...
251
252 8
		return new self(str_replace($search, $replace, $this->string), $this->encoding);
253
	}
254
255
	/**
256
	 * Replaces all occurrences of given replacement map. Keys will be replaced with its values.
257
	 *
258
	 * @param string[] $map the replacements. Keys will be replaced with its value.
259
	 *
260
	 * @return Text
261
	 */
262 1
	public function supplant(array $map): self {
263 1
		return new self(str_replace(array_keys($map), array_values($map), $this->string), $this->encoding);
264
	}
265
266
	/**
267
	 * Replace text within a portion of a string.
268
	 *
269
	 * @param string|Stringable $replacement
270
	 * @param int               $offset
271
	 * @param int|null          $length
272
	 *
273
	 * @return Text
274
	 */
275 6
	public function splice(string|Stringable $replacement, int $offset, ?int $length = null): self {
276 6
		$offset = $this->prepareOffset($offset);
277 3
		$length = $this->prepareLength($offset, $length);
278
279 1
		$start = $this->substring(0, $offset);
280 1
		$end = $this->substring($offset + $length);
281
282 1
		return new self($start . $replacement . $end);
283
	}
284
285
	//
286
	//
287
	// STRING OPERATIONS
288
	//
289
	//
290
291
	/**
292
	 * Strip whitespace (or other characters) from the beginning and end of the string
293
	 *
294
	 * @param string|Stringable $characters
295
	 *        Optionally, the stripped characters can also be specified using the mask parameter.
296
	 *        Simply list all characters that you want to be stripped. With .. you can specify a
297
	 *        range of characters.
298
	 *
299
	 * @return Text
300
	 */
301 6
	public function trim(string|Stringable $characters = " \t\n\r\v\0"): self {
302 6
		return new self(trim($this->string, (string) $characters), $this->encoding);
303
	}
304
305
	/**
306
	 * Strip whitespace (or other characters) from the beginning of the string
307
	 *
308
	 * @param string|Stringable $characters
309
	 *        Optionally, the stripped characters can also be specified using the mask parameter.
310
	 *        Simply list all characters that you want to be stripped. With .. you can specify a
311
	 *        range of characters.
312
	 *
313
	 * @return Text
314
	 */
315 1
	public function trimStart(string|Stringable $characters = " \t\n\r\v\0"): self {
316 1
		return new self(ltrim($this->string, (string) $characters), $this->encoding);
317
	}
318
319
	/**
320
	 * Strip whitespace (or other characters) from the end of the string
321
	 *
322
	 * @param string|Stringable $characters
323
	 *        Optionally, the stripped characters can also be specified using the mask parameter.
324
	 *        Simply list all characters that you want to be stripped. With .. you can specify a
325
	 *        range of characters.
326
	 *
327
	 * @return Text
328
	 */
329 1
	public function trimEnd(string|Stringable $characters = " \t\n\r\v\0"): self {
330 1
		return new self(rtrim($this->string, (string) $characters), $this->encoding);
331
	}
332
333
	/**
334
	 * Adds padding to the start and end
335
	 *
336
	 * @param int               $length
337
	 * @param string|Stringable $padding
338
	 *
339
	 * @return Text
340
	 */
341 1
	public function pad(int $length, string|Stringable $padding = ' '): self {
342 1
		$len = $length - $this->length();
343
344 1
		return $this->applyPadding(floor($len / 2), ceil($len / 2), $padding);
345
	}
346
347
	/**
348
	 * Adds padding to the start
349
	 *
350
	 * @param int               $length
351
	 * @param string|Stringable $padding
352
	 *
353
	 * @return Text
354
	 */
355 1
	public function padStart(int $length, string|Stringable $padding = ' ') {
356 1
		return $this->applyPadding($length - $this->length(), 0, $padding);
357
	}
358
359
	/**
360
	 * Adds padding to the end
361
	 *
362
	 * @param int               $length
363
	 * @param string|Stringable $padding
364
	 *
365
	 * @return Text
366
	 */
367 1
	public function padEnd(int $length, string|Stringable $padding = ' '): self {
368 1
		return $this->applyPadding(0, $length - $this->length(), $padding);
369
	}
370
371
	/**
372
	 * Adds the specified amount of left and right padding to the given string.
373
	 * The default character used is a space.
374
	 *
375
	 * @see https://github.com/danielstjules/Stringy/blob/master/src/Stringy.php
376
	 *
377
	 * @param int|float $left Length of left padding
378
	 * @param int|float $right Length of right padding
379
	 * @param string|Stringable $padStr String used to pad
380
	 *
381
	 * @return Text the padded string
382
	 */
383 1
	protected function applyPadding(int|float $left = 0, int|float $right = 0, string|Stringable $padStr = ' '): self {
384 1
		$length = mb_strlen((string) $padStr, $this->encoding);
385 1
		$strLength = $this->length();
386 1
		$paddedLength = $strLength + $left + $right;
387 1
		if (!$length || $paddedLength <= $strLength) {
388 1
			return $this;
389
		}
390
391 1
		$leftPadding = mb_substr(str_repeat((string) $padStr, (int) ceil($left / $length)), 0, (int) $left, $this->encoding);
392 1
		$rightPadding = mb_substr(str_repeat((string) $padStr, (int) ceil($right / $length)), 0, (int) $right, $this->encoding);
393
394 1
		return new self($leftPadding . $this->string . $rightPadding);
395
	}
396
397
	/**
398
	 * Ensures a given substring at the start of the string
399
	 *
400
	 * @param string $substring
401
	 *
402
	 * @return Text
403
	 */
404 1
	public function ensureStart(string $substring): self {
405 1
		if (!$this->startsWith($substring)) {
406 1
			return $this->prepend($substring);
407
		}
408
409 1
		return $this;
410
	}
411
412
	/**
413
	 * Ensures a given substring at the end of the string
414
	 *
415
	 * @param string $substring
416
	 *
417
	 * @return Text
418
	 */
419 2
	public function ensureEnd(string $substring): self {
420 2
		if (!$this->endsWith($substring)) {
421 2
			return $this->append($substring);
422
		}
423
424 1
		return $this;
425
	}
426
427
	/**
428
	 * Returns a copy of the string wrapped at a given number of characters
429
	 *
430
	 * @param int $width The number of characters at which the string will be wrapped.
431
	 * @param string $break The line is broken using the optional break parameter.
432
	 * @param bool $cut
433
	 * 		If the cut is set to TRUE, the string is always wrapped at or before the specified
434
	 * 		width. So if you have a word that is larger than the given width, it is broken apart.
435
	 *
436
	 * @return Text Returns the string wrapped at the specified length.
437
	 */
438 3
	public function wrapWords(int $width = 75, string $break = "\n", bool $cut = false): self {
439 3
		return new self(wordwrap($this->string, $width, $break, $cut), $this->encoding);
440
	}
441
442
	/**
443
	 * Repeat the string $times times. If $times is 0, it returns ''.
444
	 *
445
	 * @param int $multiplier
446
	 *
447
	 * @throws \InvalidArgumentException If $times is negative.
448
	 *
449
	 * @return Text
450
	 */
451 2
	public function repeat(int $multiplier): self {
452 2
		return new self(str_repeat($this->string, $multiplier), $this->encoding);
453
	}
454
455
	/**
456
	 * Reverses the character order
457
	 *
458
	 * @return Text
459
	 */
460 1
	public function reverse(): self {
461 1
		return new self(strrev($this->string), $this->encoding);
462
	}
463
464
	/**
465
	 * Truncates the string with a substring and ensures it doesn't exceed the given length
466
	 *
467
	 * @param int $length
468
	 * @param string $substring
469
	 *
470
	 * @return Text
471
	 */
472 1
	public function truncate(int $length, string $substring = ''): self {
473 1
		if ($this->length() <= $length) {
474 1
			return new self($this->string, $this->encoding);
475
		}
476
477 1
		$substrLen = mb_strlen($substring, $this->encoding);
478
479 1
		if ($this->length() + $substrLen > $length) {
480 1
			$length -= $substrLen;
481
		}
482
483 1
		return $this->substring(0, $length)->append($substring);
484
	}
485
486
	/**
487
	 * Returns the native string
488
	 *
489
	 * @return string
490
	 */
491 76
	public function toString(): string {
492 76
		return $this->string;
493
	}
494
495 68
	protected function getString(): string {
496 68
		return $this->toString();
497
	}
498
499
	//
500
	//
501
	// MAGIC HAPPENS HERE
502
	//
503
	//
504
505 76
	public function __toString(): string {
506 76
		return $this->string;
507
	}
508
}
509