|
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 binary-safe string of bytes. |
|
20
|
|
|
* |
|
21
|
|
|
* @author Nicolas Grekas <[email protected]> |
|
22
|
|
|
* @author Hugo Hamon <[email protected]> |
|
23
|
|
|
* |
|
24
|
|
|
* @throws ExceptionInterface |
|
25
|
|
|
*/ |
|
26
|
|
|
class ByteString extends AbstractString |
|
27
|
|
|
{ |
|
28
|
|
|
private const ALPHABET_ALPHANUMERIC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
|
29
|
|
|
|
|
30
|
|
|
public function __construct(string $string = '') |
|
31
|
|
|
{ |
|
32
|
|
|
$this->string = $string; |
|
33
|
|
|
} |
|
34
|
|
|
|
|
35
|
|
|
/* |
|
36
|
|
|
* The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03) |
|
37
|
|
|
* |
|
38
|
|
|
* https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16 |
|
39
|
|
|
* |
|
40
|
|
|
* Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE). |
|
41
|
|
|
* |
|
42
|
|
|
* Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/) |
|
43
|
|
|
*/ |
|
44
|
|
|
|
|
45
|
|
|
public static function fromRandom(int $length = 16, string $alphabet = null): self |
|
46
|
|
|
{ |
|
47
|
|
|
if ($length <= 0) { |
|
48
|
|
|
throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length)); |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
$alphabet = $alphabet ?? self::ALPHABET_ALPHANUMERIC; |
|
52
|
|
|
$alphabetSize = \strlen($alphabet); |
|
53
|
|
|
$bits = (int) ceil(log($alphabetSize, 2.0)); |
|
54
|
|
|
if ($bits <= 0 || $bits > 56) { |
|
55
|
|
|
throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); |
|
56
|
|
|
} |
|
57
|
|
|
|
|
58
|
|
|
$ret = ''; |
|
59
|
|
|
while ($length > 0) { |
|
60
|
|
|
$urandomLength = (int) ceil(2 * $length * $bits / 8.0); |
|
61
|
|
|
$data = random_bytes($urandomLength); |
|
62
|
|
|
$unpackedData = 0; |
|
63
|
|
|
$unpackedBits = 0; |
|
64
|
|
|
for ($i = 0; $i < $urandomLength && $length > 0; ++$i) { |
|
65
|
|
|
// Unpack 8 bits |
|
66
|
|
|
$unpackedData = ($unpackedData << 8) | \ord($data[$i]); |
|
67
|
|
|
$unpackedBits += 8; |
|
68
|
|
|
|
|
69
|
|
|
// While we have enough bits to select a character from the alphabet, keep |
|
70
|
|
|
// consuming the random data |
|
71
|
|
|
for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) { |
|
72
|
|
|
$index = ($unpackedData & ((1 << $bits) - 1)); |
|
73
|
|
|
$unpackedData >>= $bits; |
|
74
|
|
|
// Unfortunately, the alphabet size is not necessarily a power of two. |
|
75
|
|
|
// Worst case, it is 2^k + 1, which means we need (k+1) bits and we |
|
76
|
|
|
// have around a 50% chance of missing as k gets larger |
|
77
|
|
|
if ($index < $alphabetSize) { |
|
78
|
|
|
$ret .= $alphabet[$index]; |
|
79
|
|
|
--$length; |
|
80
|
|
|
} |
|
81
|
|
|
} |
|
82
|
|
|
} |
|
83
|
|
|
} |
|
84
|
|
|
|
|
85
|
|
|
return new static($ret); |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
public function bytesAt(int $offset): array |
|
89
|
|
|
{ |
|
90
|
|
|
$str = $this->string[$offset] ?? ''; |
|
91
|
|
|
|
|
92
|
|
|
return '' === $str ? [] : [\ord($str)]; |
|
93
|
|
|
} |
|
94
|
|
|
|
|
95
|
|
|
public function append(string ...$suffix): parent |
|
96
|
|
|
{ |
|
97
|
|
|
$str = clone $this; |
|
98
|
|
|
$str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); |
|
99
|
|
|
|
|
100
|
|
|
return $str; |
|
|
|
|
|
|
101
|
|
|
} |
|
102
|
|
|
|
|
103
|
|
|
public function camel(): parent |
|
104
|
|
|
{ |
|
105
|
|
|
$str = clone $this; |
|
106
|
|
|
$str->string = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string)))); |
|
107
|
|
|
|
|
108
|
|
|
return $str; |
|
|
|
|
|
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
public function chunk(int $length = 1): array |
|
112
|
|
|
{ |
|
113
|
|
|
if (1 > $length) { |
|
114
|
|
|
throw new InvalidArgumentException('The chunk length must be greater than zero.'); |
|
115
|
|
|
} |
|
116
|
|
|
|
|
117
|
|
|
if ('' === $this->string) { |
|
118
|
|
|
return []; |
|
119
|
|
|
} |
|
120
|
|
|
|
|
121
|
|
|
$str = clone $this; |
|
122
|
|
|
$chunks = []; |
|
123
|
|
|
|
|
124
|
|
|
foreach (str_split($this->string, $length) as $chunk) { |
|
125
|
|
|
$str->string = $chunk; |
|
126
|
|
|
$chunks[] = clone $str; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
|
return $chunks; |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
public function endsWith($suffix): bool |
|
133
|
|
|
{ |
|
134
|
|
|
if ($suffix instanceof parent) { |
|
135
|
|
|
$suffix = $suffix->string; |
|
136
|
|
|
} elseif (\is_array($suffix) || $suffix instanceof \Traversable) { |
|
137
|
|
|
return parent::endsWith($suffix); |
|
|
|
|
|
|
138
|
|
|
} else { |
|
139
|
|
|
$suffix = (string) $suffix; |
|
140
|
|
|
} |
|
141
|
|
|
|
|
142
|
|
|
return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
public function equalsTo($string): bool |
|
146
|
|
|
{ |
|
147
|
|
|
if ($string instanceof parent) { |
|
148
|
|
|
$string = $string->string; |
|
149
|
|
|
} elseif (\is_array($string) || $string instanceof \Traversable) { |
|
150
|
|
|
return parent::equalsTo($string); |
|
|
|
|
|
|
151
|
|
|
} else { |
|
152
|
|
|
$string = (string) $string; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
if ('' !== $string && $this->ignoreCase) { |
|
156
|
|
|
return 0 === strcasecmp($string, $this->string); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
return $string === $this->string; |
|
160
|
|
|
} |
|
161
|
|
|
|
|
162
|
|
|
public function folded(): parent |
|
163
|
|
|
{ |
|
164
|
|
|
$str = clone $this; |
|
165
|
|
|
$str->string = strtolower($str->string); |
|
166
|
|
|
|
|
167
|
|
|
return $str; |
|
|
|
|
|
|
168
|
|
|
} |
|
169
|
|
|
|
|
170
|
|
|
public function indexOf($needle, int $offset = 0): ?int |
|
171
|
|
|
{ |
|
172
|
|
|
if ($needle instanceof parent) { |
|
173
|
|
|
$needle = $needle->string; |
|
174
|
|
|
} elseif (\is_array($needle) || $needle instanceof \Traversable) { |
|
175
|
|
|
return parent::indexOf($needle, $offset); |
|
|
|
|
|
|
176
|
|
|
} else { |
|
177
|
|
|
$needle = (string) $needle; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
if ('' === $needle) { |
|
181
|
|
|
return null; |
|
182
|
|
|
} |
|
183
|
|
|
|
|
184
|
|
|
$i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); |
|
185
|
|
|
|
|
186
|
|
|
return false === $i ? null : $i; |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
public function indexOfLast($needle, int $offset = 0): ?int |
|
190
|
|
|
{ |
|
191
|
|
|
if ($needle instanceof parent) { |
|
192
|
|
|
$needle = $needle->string; |
|
193
|
|
|
} elseif (\is_array($needle) || $needle instanceof \Traversable) { |
|
194
|
|
|
return parent::indexOfLast($needle, $offset); |
|
|
|
|
|
|
195
|
|
|
} else { |
|
196
|
|
|
$needle = (string) $needle; |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
if ('' === $needle) { |
|
200
|
|
|
return null; |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
$i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); |
|
204
|
|
|
|
|
205
|
|
|
return false === $i ? null : $i; |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
public function isUtf8(): bool |
|
209
|
|
|
{ |
|
210
|
|
|
return '' === $this->string || preg_match('//u', $this->string); |
|
211
|
|
|
} |
|
212
|
|
|
|
|
213
|
|
|
public function join(array $strings, string $lastGlue = null): parent |
|
214
|
|
|
{ |
|
215
|
|
|
$str = clone $this; |
|
216
|
|
|
|
|
217
|
|
|
$tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; |
|
218
|
|
|
$str->string = implode($this->string, $strings).$tail; |
|
219
|
|
|
|
|
220
|
|
|
return $str; |
|
|
|
|
|
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
public function length(): int |
|
224
|
|
|
{ |
|
225
|
|
|
return \strlen($this->string); |
|
226
|
|
|
} |
|
227
|
|
|
|
|
228
|
|
|
public function lower(): parent |
|
229
|
|
|
{ |
|
230
|
|
|
$str = clone $this; |
|
231
|
|
|
$str->string = strtolower($str->string); |
|
232
|
|
|
|
|
233
|
|
|
return $str; |
|
|
|
|
|
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
public function match(string $regexp, int $flags = 0, int $offset = 0): array |
|
237
|
|
|
{ |
|
238
|
|
|
$match = ((PREG_PATTERN_ORDER | PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; |
|
239
|
|
|
|
|
240
|
|
|
if ($this->ignoreCase) { |
|
241
|
|
|
$regexp .= 'i'; |
|
242
|
|
|
} |
|
243
|
|
|
|
|
244
|
|
|
set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); |
|
245
|
|
|
|
|
246
|
|
|
try { |
|
247
|
|
|
if (false === $match($regexp, $this->string, $matches, $flags | PREG_UNMATCHED_AS_NULL, $offset)) { |
|
|
|
|
|
|
248
|
|
|
$lastError = preg_last_error(); |
|
249
|
|
|
|
|
250
|
|
|
foreach (get_defined_constants(true)['pcre'] as $k => $v) { |
|
251
|
|
|
if ($lastError === $v && '_ERROR' === substr($k, -6)) { |
|
252
|
|
|
throw new RuntimeException('Matching failed with '.$k.'.'); |
|
253
|
|
|
} |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
throw new RuntimeException('Matching failed with unknown error code.'); |
|
257
|
|
|
} |
|
258
|
|
|
} finally { |
|
259
|
|
|
restore_error_handler(); |
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
return $matches; |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
public function padBoth(int $length, string $padStr = ' '): parent |
|
266
|
|
|
{ |
|
267
|
|
|
$str = clone $this; |
|
268
|
|
|
$str->string = str_pad($this->string, $length, $padStr, STR_PAD_BOTH); |
|
269
|
|
|
|
|
270
|
|
|
return $str; |
|
|
|
|
|
|
271
|
|
|
} |
|
272
|
|
|
|
|
273
|
|
|
public function padEnd(int $length, string $padStr = ' '): parent |
|
274
|
|
|
{ |
|
275
|
|
|
$str = clone $this; |
|
276
|
|
|
$str->string = str_pad($this->string, $length, $padStr, STR_PAD_RIGHT); |
|
277
|
|
|
|
|
278
|
|
|
return $str; |
|
|
|
|
|
|
279
|
|
|
} |
|
280
|
|
|
|
|
281
|
|
|
public function padStart(int $length, string $padStr = ' '): parent |
|
282
|
|
|
{ |
|
283
|
|
|
$str = clone $this; |
|
284
|
|
|
$str->string = str_pad($this->string, $length, $padStr, STR_PAD_LEFT); |
|
285
|
|
|
|
|
286
|
|
|
return $str; |
|
|
|
|
|
|
287
|
|
|
} |
|
288
|
|
|
|
|
289
|
|
|
public function prepend(string ...$prefix): parent |
|
290
|
|
|
{ |
|
291
|
|
|
$str = clone $this; |
|
292
|
|
|
$str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string; |
|
293
|
|
|
|
|
294
|
|
|
return $str; |
|
|
|
|
|
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
public function replace(string $from, string $to): parent |
|
298
|
|
|
{ |
|
299
|
|
|
$str = clone $this; |
|
300
|
|
|
|
|
301
|
|
|
if ('' !== $from) { |
|
302
|
|
|
$str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string); |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
return $str; |
|
|
|
|
|
|
306
|
|
|
} |
|
307
|
|
|
|
|
308
|
|
|
public function replaceMatches(string $fromRegexp, $to): parent |
|
309
|
|
|
{ |
|
310
|
|
|
if ($this->ignoreCase) { |
|
311
|
|
|
$fromRegexp .= 'i'; |
|
312
|
|
|
} |
|
313
|
|
|
|
|
314
|
|
|
if (\is_array($to)) { |
|
315
|
|
|
if (!\is_callable($to)) { |
|
316
|
|
|
throw new \TypeError(sprintf('Argument 2 passed to "%s::replaceMatches()" must be callable, array given.', static::class)); |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
$replace = 'preg_replace_callback'; |
|
320
|
|
|
} else { |
|
321
|
|
|
$replace = $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace'; |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); |
|
325
|
|
|
|
|
326
|
|
|
try { |
|
327
|
|
|
if (null === $string = $replace($fromRegexp, $to, $this->string)) { |
|
328
|
|
|
$lastError = preg_last_error(); |
|
329
|
|
|
|
|
330
|
|
|
foreach (get_defined_constants(true)['pcre'] as $k => $v) { |
|
331
|
|
|
if ($lastError === $v && '_ERROR' === substr($k, -6)) { |
|
332
|
|
|
throw new RuntimeException('Matching failed with '.$k.'.'); |
|
333
|
|
|
} |
|
334
|
|
|
} |
|
335
|
|
|
|
|
336
|
|
|
throw new RuntimeException('Matching failed with unknown error code.'); |
|
337
|
|
|
} |
|
338
|
|
|
} finally { |
|
339
|
|
|
restore_error_handler(); |
|
340
|
|
|
} |
|
341
|
|
|
|
|
342
|
|
|
$str = clone $this; |
|
343
|
|
|
$str->string = $string; |
|
344
|
|
|
|
|
345
|
|
|
return $str; |
|
|
|
|
|
|
346
|
|
|
} |
|
347
|
|
|
|
|
348
|
|
|
public function reverse(): parent |
|
349
|
|
|
{ |
|
350
|
|
|
$str = clone $this; |
|
351
|
|
|
$str->string = strrev($str->string); |
|
352
|
|
|
|
|
353
|
|
|
return $str; |
|
|
|
|
|
|
354
|
|
|
} |
|
355
|
|
|
|
|
356
|
|
|
public function slice(int $start = 0, int $length = null): parent |
|
357
|
|
|
{ |
|
358
|
|
|
$str = clone $this; |
|
359
|
|
|
$str->string = (string) substr($this->string, $start, $length ?? PHP_INT_MAX); |
|
360
|
|
|
|
|
361
|
|
|
return $str; |
|
|
|
|
|
|
362
|
|
|
} |
|
363
|
|
|
|
|
364
|
|
|
public function snake(): parent |
|
365
|
|
|
{ |
|
366
|
|
|
$str = $this->camel()->title(); |
|
367
|
|
|
$str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); |
|
368
|
|
|
|
|
369
|
|
|
return $str; |
|
370
|
|
|
} |
|
371
|
|
|
|
|
372
|
|
|
public function splice(string $replacement, int $start = 0, int $length = null): parent |
|
373
|
|
|
{ |
|
374
|
|
|
$str = clone $this; |
|
375
|
|
|
$str->string = substr_replace($this->string, $replacement, $start, $length ?? PHP_INT_MAX); |
|
376
|
|
|
|
|
377
|
|
|
return $str; |
|
|
|
|
|
|
378
|
|
|
} |
|
379
|
|
|
|
|
380
|
|
|
public function split(string $delimiter, int $limit = null, int $flags = null): array |
|
381
|
|
|
{ |
|
382
|
|
|
if (1 > $limit = $limit ?? PHP_INT_MAX) { |
|
383
|
|
|
throw new InvalidArgumentException('Split limit must be a positive integer.'); |
|
384
|
|
|
} |
|
385
|
|
|
|
|
386
|
|
|
if ('' === $delimiter) { |
|
387
|
|
|
throw new InvalidArgumentException('Split delimiter is empty.'); |
|
388
|
|
|
} |
|
389
|
|
|
|
|
390
|
|
|
if (null !== $flags) { |
|
391
|
|
|
return parent::split($delimiter, $limit, $flags); |
|
392
|
|
|
} |
|
393
|
|
|
|
|
394
|
|
|
$str = clone $this; |
|
395
|
|
|
$chunks = $this->ignoreCase |
|
396
|
|
|
? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit) |
|
397
|
|
|
: explode($delimiter, $this->string, $limit); |
|
398
|
|
|
|
|
399
|
|
|
foreach ($chunks as &$chunk) { |
|
400
|
|
|
$str->string = $chunk; |
|
401
|
|
|
$chunk = clone $str; |
|
402
|
|
|
} |
|
403
|
|
|
|
|
404
|
|
|
return $chunks; |
|
405
|
|
|
} |
|
406
|
|
|
|
|
407
|
|
|
public function startsWith($prefix): bool |
|
408
|
|
|
{ |
|
409
|
|
|
if ($prefix instanceof parent) { |
|
410
|
|
|
$prefix = $prefix->string; |
|
411
|
|
|
} elseif (!\is_string($prefix)) { |
|
412
|
|
|
return parent::startsWith($prefix); |
|
413
|
|
|
} |
|
414
|
|
|
|
|
415
|
|
|
return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
public function title(bool $allWords = false): parent |
|
419
|
|
|
{ |
|
420
|
|
|
$str = clone $this; |
|
421
|
|
|
$str->string = $allWords ? ucwords($str->string) : ucfirst($str->string); |
|
422
|
|
|
|
|
423
|
|
|
return $str; |
|
|
|
|
|
|
424
|
|
|
} |
|
425
|
|
|
|
|
426
|
|
|
public function toUnicodeString(string $fromEncoding = null): UnicodeString |
|
427
|
|
|
{ |
|
428
|
|
|
return new UnicodeString($this->toCodePointString($fromEncoding)->string); |
|
|
|
|
|
|
429
|
|
|
} |
|
430
|
|
|
|
|
431
|
|
|
public function toCodePointString(string $fromEncoding = null): CodePointString |
|
432
|
|
|
{ |
|
433
|
|
|
$u = new CodePointString(); |
|
434
|
|
|
|
|
435
|
|
|
if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) { |
|
436
|
|
|
$u->string = $this->string; |
|
|
|
|
|
|
437
|
|
|
|
|
438
|
|
|
return $u; |
|
439
|
|
|
} |
|
440
|
|
|
|
|
441
|
|
|
set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); }); |
|
442
|
|
|
|
|
443
|
|
|
try { |
|
444
|
|
|
try { |
|
445
|
|
|
$validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true); |
|
446
|
|
|
} catch (InvalidArgumentException $e) { |
|
447
|
|
|
if (!\function_exists('iconv')) { |
|
448
|
|
|
throw $e; |
|
449
|
|
|
} |
|
450
|
|
|
|
|
451
|
|
|
$u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string); |
|
|
|
|
|
|
452
|
|
|
|
|
453
|
|
|
return $u; |
|
454
|
|
|
} |
|
455
|
|
|
} finally { |
|
456
|
|
|
restore_error_handler(); |
|
457
|
|
|
} |
|
458
|
|
|
|
|
459
|
|
|
if (!$validEncoding) { |
|
460
|
|
|
throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); |
|
461
|
|
|
} |
|
462
|
|
|
|
|
463
|
|
|
$u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); |
|
|
|
|
|
|
464
|
|
|
|
|
465
|
|
|
return $u; |
|
466
|
|
|
} |
|
467
|
|
|
|
|
468
|
|
|
public function trim(string $chars = " \t\n\r\0\x0B\x0C"): parent |
|
469
|
|
|
{ |
|
470
|
|
|
$str = clone $this; |
|
471
|
|
|
$str->string = trim($str->string, $chars); |
|
472
|
|
|
|
|
473
|
|
|
return $str; |
|
|
|
|
|
|
474
|
|
|
} |
|
475
|
|
|
|
|
476
|
|
|
public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): parent |
|
477
|
|
|
{ |
|
478
|
|
|
$str = clone $this; |
|
479
|
|
|
$str->string = rtrim($str->string, $chars); |
|
480
|
|
|
|
|
481
|
|
|
return $str; |
|
|
|
|
|
|
482
|
|
|
} |
|
483
|
|
|
|
|
484
|
|
|
public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): parent |
|
485
|
|
|
{ |
|
486
|
|
|
$str = clone $this; |
|
487
|
|
|
$str->string = ltrim($str->string, $chars); |
|
488
|
|
|
|
|
489
|
|
|
return $str; |
|
|
|
|
|
|
490
|
|
|
} |
|
491
|
|
|
|
|
492
|
|
|
public function upper(): parent |
|
493
|
|
|
{ |
|
494
|
|
|
$str = clone $this; |
|
495
|
|
|
$str->string = strtoupper($str->string); |
|
496
|
|
|
|
|
497
|
|
|
return $str; |
|
|
|
|
|
|
498
|
|
|
} |
|
499
|
|
|
|
|
500
|
|
|
public function width(bool $ignoreAnsiDecoration = true): int |
|
501
|
|
|
{ |
|
502
|
|
|
$string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string); |
|
503
|
|
|
|
|
504
|
|
|
return (new CodePointString($string))->width($ignoreAnsiDecoration); |
|
505
|
|
|
} |
|
506
|
|
|
} |
|
507
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_functionexpects aPostobject, and outputs the author of the post. The base classPostreturns a simple string and outputting a simple string will work just fine. However, the child classBlogPostwhich is a sub-type ofPostinstead decided to return anobject, and is therefore violating the SOLID principles. If aBlogPostwere passed tomy_function, PHP would not complain, but ultimately fail when executing thestrtouppercall in its body.