1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Tdn\PhpTypes\Type; |
6
|
|
|
|
7
|
|
|
use Doctrine\Common\Inflector\Inflector; |
8
|
|
|
use Doctrine\Common\Collections\Collection as CollectionInterface; |
9
|
|
|
use Stringy\Stringy; |
10
|
|
|
use Tdn\PhpTypes\Exception\InvalidTypeCastException; |
11
|
|
|
use Tdn\PhpTypes\Type\Traits\Boxable; |
12
|
|
|
use Tdn\PhpTypes\Type\Traits\Transmutable; |
13
|
|
|
use Tdn\PhpTypes\Exception\InvalidTransformationException; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Class StringType. |
17
|
|
|
* |
18
|
|
|
* A StringType is a TypeInterface implementation that wraps around a regular PHP string. |
19
|
|
|
* This object extends Stringy. |
20
|
|
|
* |
21
|
|
|
* {@inheritdoc} |
22
|
|
|
*/ |
23
|
|
|
class StringType extends Stringy implements TransmutableTypeInterface, ValueTypeInterface |
24
|
|
|
{ |
25
|
|
|
use Transmutable; |
26
|
|
|
use Boxable; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* StringType constructor. |
30
|
|
|
* Explicitly removing parent constructor. Type conversion should be done through StringType::from($mixed). |
31
|
|
|
* |
32
|
|
|
* @param string $str |
33
|
|
|
* @param string|null $encoding |
34
|
|
|
*/ |
35
|
91 |
|
public function __construct(string $str, string $encoding = null) |
36
|
|
|
{ |
37
|
91 |
|
$this->str = $str; |
38
|
91 |
|
$this->encoding = $encoding ?: \mb_internal_encoding(); |
39
|
|
|
//Explicitly not calling parent construct. |
40
|
91 |
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* {@inheritdoc} |
44
|
|
|
* |
45
|
|
|
* @return string|int|float|bool|array |
46
|
|
|
*/ |
47
|
20 |
|
public function __invoke(int $toType = Type::STRING) |
48
|
|
|
{ |
49
|
|
|
switch ($toType) { |
50
|
20 |
|
case Type::STRING: |
51
|
11 |
|
return $this->str; |
52
|
10 |
|
case Type::INT: |
53
|
2 |
|
if (is_numeric((string) $this)) { |
54
|
1 |
|
return IntType::valueOf($this)->get(); |
55
|
|
|
} |
56
|
|
|
|
57
|
1 |
|
break; |
58
|
9 |
|
case Type::FLOAT: |
59
|
2 |
|
if (is_numeric((string) $this)) { |
60
|
1 |
|
return FloatType::valueOf($this)->get(); |
61
|
|
|
} |
62
|
|
|
|
63
|
1 |
|
break; |
64
|
8 |
|
case Type::ARRAY: |
65
|
2 |
|
if ($this->contains(',')) { |
66
|
1 |
|
return $this->explode(',')->toArray(); |
67
|
|
|
} |
68
|
|
|
|
69
|
1 |
|
break; |
70
|
7 |
|
case Type::BOOL: |
71
|
1 |
|
return BooleanType::valueOf($this)->get(); |
72
|
|
|
} |
73
|
|
|
|
74
|
9 |
|
throw new InvalidTypeCastException(static::class, $this->getTranslatedType($toType)); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @return string |
79
|
|
|
*/ |
80
|
35 |
|
public function get() |
81
|
|
|
{ |
82
|
35 |
|
return (string) $this; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Creates a new static instance from string. |
87
|
|
|
* |
88
|
|
|
* Mainly here for code completion purposes... |
89
|
|
|
* |
90
|
|
|
* @param string $str |
91
|
|
|
* @param string $encoding |
92
|
|
|
* |
93
|
|
|
* @return StringType |
94
|
|
|
*/ |
95
|
51 |
|
public static function create($str = '', $encoding = 'UTF-8'): StringType |
96
|
|
|
{ |
97
|
51 |
|
return new static($str, $encoding); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Explodes current instance into a collection object. |
102
|
|
|
* |
103
|
|
|
* @param string $delimiter |
104
|
|
|
* @param int $limit default PHP_INT_MAX |
105
|
|
|
* @param bool $trim default false, greedely trim the string before exploding |
106
|
|
|
* |
107
|
|
|
* @return Collection|StringType[] |
108
|
|
|
*/ |
109
|
8 |
|
public function explode(string $delimiter, int $limit = PHP_INT_MAX, bool $trim = true): Collection |
110
|
|
|
{ |
111
|
8 |
|
$str = ($trim) ? $this->regexReplace('[[:space:]]', '')->str : $this->str; |
112
|
8 |
|
$delimiter = ($trim) ? static::create($delimiter)->regexReplace('[[:space:]]', '')->str : $delimiter; |
113
|
|
|
|
114
|
8 |
|
return new Collection(explode($delimiter, $str, $limit), static::class); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Pluralizes the string. |
119
|
|
|
* |
120
|
|
|
* @return StringType |
121
|
|
|
*/ |
122
|
1 |
|
public function pluralize(): StringType |
123
|
|
|
{ |
124
|
1 |
|
return static::create($this->getInflector()->pluralize((string) $this->str), $this->encoding); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Singularizes the string. |
129
|
|
|
* |
130
|
|
|
* @return StringType |
131
|
|
|
*/ |
132
|
1 |
|
public function singularize(): StringType |
133
|
|
|
{ |
134
|
1 |
|
return static::create($this->getInflector()->singularize((string) $this->str), $this->encoding); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Returns position of the first occurrence of subStr null if not present. |
139
|
|
|
* |
140
|
|
|
* @param string $subStr Substring |
141
|
|
|
* @param int $offset Chars to offset from start |
142
|
|
|
* @param bool $caseSensitive Enable case sensitivity |
143
|
|
|
* |
144
|
|
|
* @return IntType |
145
|
|
|
*/ |
146
|
3 |
|
public function strpos(string $subStr, int $offset = 0, bool $caseSensitive = false): IntType |
|
|
|
|
147
|
|
|
{ |
148
|
3 |
|
$res = ($caseSensitive) ? |
149
|
1 |
|
mb_strpos($this->str, $subStr, $offset, $this->encoding) : |
150
|
3 |
|
mb_stripos($this->str, $subStr, $offset, $this->encoding); |
151
|
|
|
|
152
|
3 |
|
return new IntType($res); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Returns position of the last occurrence of subStr null if not present. |
157
|
|
|
* |
158
|
|
|
* @param string $subStr Substring |
159
|
|
|
* @param int $offset Chars to offset from start |
160
|
|
|
* @param bool $caseSensitive Enable case sensitivity |
161
|
|
|
* |
162
|
|
|
* @return IntType |
163
|
|
|
*/ |
164
|
1 |
|
public function strrpos(string $subStr, int $offset = 0, bool $caseSensitive = false): IntType |
|
|
|
|
165
|
|
|
{ |
166
|
1 |
|
$res = ($caseSensitive) ? |
167
|
1 |
|
mb_strrpos($this->str, $subStr, $offset, $this->encoding) : |
168
|
1 |
|
mb_strripos($this->str, $subStr, $offset, $this->encoding); |
169
|
|
|
|
170
|
1 |
|
return new IntType($res); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @return BooleanType |
175
|
|
|
*/ |
176
|
6 |
|
public function isSemVer(): BooleanType |
177
|
|
|
{ |
178
|
6 |
|
$parts = $this->explode('.'); |
179
|
6 |
|
foreach ($parts as $possibleNumber) { |
180
|
6 |
|
if (false === is_numeric($possibleNumber->toString())) { |
181
|
6 |
|
return new BooleanType(false); |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
6 |
|
return ($this->countSubstr('.') > 1) ? new BooleanType(true) : new BooleanType(false); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @return string The current value of the $str property |
190
|
|
|
*/ |
191
|
43 |
|
public function __toString(): string |
192
|
|
|
{ |
193
|
43 |
|
return (string) $this->str; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Override trait to remove spaces. |
198
|
|
|
* |
199
|
|
|
* @return BooleanType |
200
|
|
|
*/ |
201
|
1 |
|
public function toBoolType(): BooleanType |
202
|
|
|
{ |
203
|
1 |
|
return BooleanType::valueOf($this->regexReplace('[[:space:]]', '')->str); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* @return BooleanType |
208
|
|
|
*/ |
209
|
1 |
|
public function toBoolean(): BooleanType |
210
|
|
|
{ |
211
|
1 |
|
return $this->toBoolType(); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* @return DateTimeType |
216
|
|
|
*/ |
217
|
1 |
|
public function toDateTime(): DateTimeType |
218
|
|
|
{ |
219
|
1 |
|
return DateTimeType::valueOf($this); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* {@inheritdoc} |
224
|
|
|
* |
225
|
|
|
* @return StringType |
226
|
|
|
*/ |
227
|
54 |
|
public static function valueOf($mixed): StringType |
228
|
|
|
{ |
229
|
54 |
|
return new static(self::asString($mixed)); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Returns substring from beginning until first instance of subsStr. |
234
|
|
|
* |
235
|
|
|
* @param string $subStr |
236
|
|
|
* @param bool $includingSubStr |
237
|
|
|
* @param bool $caseSensitive |
238
|
|
|
* |
239
|
|
|
* @return static |
240
|
|
|
*/ |
241
|
1 |
|
public function subStrUntil($subStr, $includingSubStr = false, $caseSensitive = false) |
242
|
|
|
{ |
243
|
1 |
|
$fromSubStr = $this->str[0]; |
244
|
|
|
|
245
|
1 |
|
return $this->subStrBetween($fromSubStr, $subStr, false, !$includingSubStr, $caseSensitive); |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Returns substring from first instance of subStr to end of string. |
250
|
|
|
* @param $subStr |
251
|
|
|
* @param bool $includingSubStr |
252
|
|
|
* @param bool $caseSensitive |
253
|
|
|
* |
254
|
|
|
* @return static |
255
|
|
|
*/ |
256
|
1 |
|
public function subStrAfter($subStr, $includingSubStr = false, $caseSensitive = false) |
257
|
|
|
{ |
258
|
1 |
|
return $this->subStrBetween($subStr, null, !$includingSubStr, false, $caseSensitive); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Returns substring between fromSubStr to toSubStr. End of string if toSubStr is not set. |
263
|
|
|
* |
264
|
|
|
* @param string $fromSubStr |
265
|
|
|
* @param string $toSubStr |
266
|
|
|
* @param bool $excludeFromSubStr |
267
|
|
|
* @param bool $excludeToSubStr |
268
|
|
|
* @param bool $caseSensitive |
269
|
|
|
* @return self |
270
|
|
|
*/ |
271
|
2 |
|
private function subStrBetween( |
272
|
|
|
$fromSubStr, |
273
|
|
|
$toSubStr = '', |
274
|
|
|
$excludeFromSubStr = false, |
275
|
|
|
$excludeToSubStr = false, |
276
|
|
|
$caseSensitive = false |
277
|
|
|
) { |
278
|
2 |
|
$fromIndex = 0; |
279
|
2 |
|
$toIndex = mb_strlen($this->str); |
280
|
2 |
|
$str = self::create($this->str); |
281
|
2 |
|
if ($str->contains($fromSubStr)) { |
282
|
2 |
|
$fromIndex = $this->strpos($fromSubStr, 0, $caseSensitive)->get(); |
283
|
2 |
|
$fromIndex = ($excludeFromSubStr) ? $fromIndex + mb_strlen($fromSubStr, $this->encoding) : $fromIndex; |
284
|
2 |
|
if ($fromIndex < 0) { |
285
|
|
|
throw new \LogicException('To cannot be before from.'); |
286
|
|
|
} |
287
|
2 |
|
if (!empty($toSubStr) && $str->contains($toSubStr)) { |
288
|
1 |
|
$toIndex = $this->strpos($toSubStr, $fromIndex, $caseSensitive)->get(); |
289
|
1 |
|
$toIndex = ($excludeToSubStr) ? |
290
|
1 |
|
$toIndex - $fromIndex : ($toIndex - $fromIndex) + mb_strlen($toSubStr, $this->encoding); |
291
|
|
|
} |
292
|
|
|
} |
293
|
|
|
|
294
|
2 |
|
return ($toSubStr) ? $str->substr($fromIndex, $toIndex) : $str->substr($fromIndex); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* @return Inflector |
299
|
|
|
*/ |
300
|
2 |
|
private function getInflector(): Inflector |
301
|
|
|
{ |
302
|
2 |
|
return new Inflector(); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Returns a mixed variable as a string. |
307
|
|
|
* |
308
|
|
|
* @param mixed $mixed |
309
|
|
|
* |
310
|
|
|
* @return string |
311
|
|
|
*/ |
312
|
54 |
|
private static function asString($mixed): string |
313
|
|
|
{ |
314
|
54 |
|
if ($mixed instanceof ValueTypeInterface) { |
315
|
35 |
|
$mixed = $mixed->get(); |
316
|
|
|
} |
317
|
|
|
|
318
|
54 |
|
if ($mixed instanceof CollectionInterface) { |
319
|
|
|
//Handle as array. |
320
|
4 |
|
$mixed = $mixed->toArray(); |
321
|
|
|
} |
322
|
|
|
|
323
|
54 |
|
$type = strtolower(gettype($mixed)); |
324
|
|
|
switch ($type) { |
325
|
54 |
|
case 'string': |
326
|
51 |
|
case 'integer': |
327
|
36 |
|
case 'float': |
328
|
36 |
|
case 'double': |
329
|
49 |
|
return (string) $mixed; |
330
|
6 |
|
case 'boolean': |
331
|
1 |
|
return ($mixed) ? 'true' : 'false'; |
332
|
6 |
|
case 'array': |
333
|
4 |
|
return implode(', ', $mixed); |
334
|
3 |
|
case 'object': |
335
|
2 |
|
if (method_exists($mixed, '__toString')) { |
336
|
1 |
|
return (string) $mixed; |
337
|
|
|
} |
338
|
|
|
|
339
|
1 |
|
throw new InvalidTransformationException($type, static::class); |
340
|
2 |
|
case 'resource': |
341
|
1 |
|
return get_resource_type($mixed); |
342
|
1 |
|
case 'null': |
343
|
|
|
default: |
344
|
1 |
|
throw new InvalidTransformationException($type, static::class); |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
|
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.