1
|
|
|
<?php |
2
|
|
|
namespace Fwlib\Util\Common; |
3
|
|
|
|
4
|
|
|
/** |
5
|
|
|
* String util |
6
|
|
|
* |
7
|
|
|
* Util class is collection of functions, will not keep state, so method |
8
|
|
|
* amount and class complexity should have no limit. |
9
|
|
|
* @SuppressWarnings(PHPMD.TooManyMethods) |
10
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) |
11
|
|
|
* |
12
|
|
|
* @copyright Copyright 2004-2015 Fwolf |
13
|
|
|
* @license http://www.gnu.org/licenses/lgpl.html LGPL-3.0+ |
14
|
|
|
*/ |
15
|
|
|
class StringUtil |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* Addslashes for any string|array, recursive |
19
|
|
|
* |
20
|
|
|
* @param mixed $source |
21
|
|
|
* @return mixed |
22
|
|
|
*/ |
23
|
|
View Code Duplication |
public function addSlashesRecursive($source) |
|
|
|
|
24
|
|
|
{ |
25
|
|
|
if (empty($source)) { |
26
|
|
|
return $source; |
27
|
|
|
} |
28
|
|
|
|
29
|
|
|
if (is_string($source)) { |
30
|
|
|
return addslashes($source); |
31
|
|
|
} elseif (is_array($source)) { |
32
|
|
|
$rs = []; |
33
|
|
|
foreach ($source as $k => $v) { |
34
|
|
|
$rs[addslashes($k)] = $this->addSlashesRecursive($v); |
35
|
|
|
} |
36
|
|
|
return $rs; |
37
|
|
|
} else { |
38
|
|
|
// Other data type, return original |
39
|
|
|
return $source; |
40
|
|
|
} |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Encode string for html output |
46
|
|
|
* |
47
|
|
|
* @param string $str |
48
|
|
|
* @param boolean $stripSlashes |
49
|
|
|
* @param boolean $nl2br |
50
|
|
|
* @param boolean $optimizeSpaces |
51
|
|
|
* @return string |
52
|
|
|
*/ |
53
|
|
|
public function encodeHtml( |
54
|
|
|
$str, |
55
|
|
|
$stripSlashes = true, |
56
|
|
|
$nl2br = true, |
57
|
|
|
$optimizeSpaces = true |
58
|
|
|
) { |
59
|
|
|
if ($stripSlashes) { |
60
|
|
|
$str = stripSlashes($str); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
$str = htmlentities($str, ENT_QUOTES, 'UTF-8'); |
64
|
|
|
|
65
|
|
|
if ($optimizeSpaces) { |
66
|
|
|
$ar = [ |
67
|
|
|
' ' => ' ', |
68
|
|
|
' ' => ' ', |
69
|
|
|
' ' => ' ', |
70
|
|
|
]; |
71
|
|
|
$str = str_replace(array_keys($ar), array_values($ar), $str); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
if ($nl2br) { |
75
|
|
|
$str = nl2br($str, true); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
return $str; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Encode array of string for html output |
84
|
|
|
* |
85
|
|
|
* @param array $stringArray |
86
|
|
|
* @param boolean $stripSlashes |
87
|
|
|
* @param boolean $nl2br |
88
|
|
|
* @param boolean $optimizeSpaces |
89
|
|
|
* @return string |
90
|
|
|
*/ |
91
|
|
|
public function encodeHtmls( |
92
|
|
|
array $stringArray, |
93
|
|
|
$stripSlashes = true, |
94
|
|
|
$nl2br = true, |
95
|
|
|
$optimizeSpaces = true |
96
|
|
|
) { |
97
|
|
|
foreach ($stringArray as &$string) { |
98
|
|
|
$string = $this->encodeHtml( |
99
|
|
|
$string, |
100
|
|
|
$stripSlashes, |
101
|
|
|
$nl2br, |
102
|
|
|
$optimizeSpaces |
103
|
|
|
); |
104
|
|
|
} |
105
|
|
|
unset($string); |
106
|
|
|
|
107
|
|
|
return $stringArray; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Indent a string |
113
|
|
|
* |
114
|
|
|
* The first line will also be indented. |
115
|
|
|
* |
116
|
|
|
* Commonly used in generate and combine html, fix indents. |
117
|
|
|
* |
118
|
|
|
* The $spacer should have width 1, if not, the real indent width is |
119
|
|
|
* mb_strwidth($spacer) * $width . |
120
|
|
|
* |
121
|
|
|
* @param string $str |
122
|
|
|
* @param int $width Must > 0 |
123
|
|
|
* @param string $spacer Which char is used to indent |
124
|
|
|
* @param string $lineEnding Original string's line ending |
125
|
|
|
* @param bool $fillEmptyLine Add spacer to empty line ? |
126
|
|
|
* @return string |
127
|
|
|
*/ |
128
|
|
|
public function indent( |
129
|
|
|
$str, |
130
|
|
|
$width, |
131
|
|
|
$spacer = ' ', |
132
|
|
|
$lineEnding = "\n", |
133
|
|
|
$fillEmptyLine = false |
134
|
|
|
) { |
135
|
|
|
$space = str_repeat($spacer, $width); |
136
|
|
|
|
137
|
|
|
$lines = explode($lineEnding, $str); |
138
|
|
|
|
139
|
|
|
array_walk($lines, function(&$value) use ($fillEmptyLine, $space) { |
140
|
|
|
if ($fillEmptyLine || !empty($value)) { |
141
|
|
|
$value = $space . $value; |
142
|
|
|
} |
143
|
|
|
}); |
144
|
|
|
|
145
|
|
|
$str = implode($lineEnding, $lines); |
146
|
|
|
|
147
|
|
|
return $str; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* Indent a html string, except value of some html tag like textarea |
153
|
|
|
* |
154
|
|
|
* Html string should have both start and end tag of html tag. |
155
|
|
|
* |
156
|
|
|
* Works for html tag: |
157
|
|
|
* - textarea |
158
|
|
|
|
159
|
|
|
* @param string $html |
160
|
|
|
* @param int $width Must > 0 |
161
|
|
|
* @param string $spacer Which char is used to indent |
162
|
|
|
* @param string $lineEnding Original string's line ending |
163
|
|
|
* @param bool $fillEmptyLine Add spacer to empty line ? |
164
|
|
|
* @return string |
165
|
|
|
*/ |
166
|
|
|
public function indentHtml( |
167
|
|
|
$html, |
168
|
|
|
$width, |
169
|
|
|
$spacer = ' ', |
170
|
|
|
$lineEnding = "\n", |
171
|
|
|
$fillEmptyLine = false |
172
|
|
|
) { |
173
|
|
|
// Find textarea start point |
174
|
|
|
$i = stripos($html, '<textarea>'); |
175
|
|
|
if (false === $i) { |
176
|
|
|
$i = stripos($html, '<textarea '); |
177
|
|
|
} |
178
|
|
|
if (false === $i) { |
179
|
|
|
return $this->indent($html, $width, $spacer, $lineEnding); |
180
|
|
|
|
181
|
|
|
} else { |
182
|
|
|
$htmlBefore = substr($html, 0, $i); |
183
|
|
|
$htmlBefore = $this->indent( |
184
|
|
|
$htmlBefore, |
185
|
|
|
$width, |
186
|
|
|
$spacer, |
187
|
|
|
$lineEnding, |
188
|
|
|
$fillEmptyLine |
189
|
|
|
); |
190
|
|
|
|
191
|
|
|
// Find textarea end point |
192
|
|
|
$html = substr($html, $i); |
193
|
|
|
$i = stripos($html, '</textarea>'); |
194
|
|
|
if (false === $i) { |
195
|
|
|
// Should not happen, source html format error |
196
|
|
|
$htmlAfter = ''; |
197
|
|
|
|
198
|
|
|
} else { |
199
|
|
|
$i += strlen('</textarea>'); |
200
|
|
|
|
201
|
|
|
$htmlAfter = substr($html, $i); |
202
|
|
|
// In case there are another textarea in it |
203
|
|
|
$htmlAfter = $this->indentHtml( |
204
|
|
|
$htmlAfter, |
205
|
|
|
$width, |
206
|
|
|
$spacer, |
207
|
|
|
$lineEnding, |
208
|
|
|
$fillEmptyLine |
209
|
|
|
); |
210
|
|
|
// Remove leading space except start with new line |
211
|
|
|
if ($fillEmptyLine || |
212
|
|
|
substr($htmlAfter, 0, strlen($lineEnding)) != $lineEnding |
213
|
|
|
) { |
214
|
|
|
$htmlAfter = substr($htmlAfter, $width); |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
$html = substr($html, 0, $i); |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return $htmlBefore . $html . $htmlAfter; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Match content using preg, return result array or string |
227
|
|
|
* |
228
|
|
|
* Return value maybe string or array, use with caution. |
229
|
|
|
* |
230
|
|
|
* @param string $preg |
231
|
|
|
* @param string $str |
232
|
|
|
* @param boolean $simple Convert single result to str(array -> str) ? |
233
|
|
|
* @return string|array|null |
234
|
|
|
*/ |
235
|
|
|
public function matchRegex($preg, $str = '', $simple = true) |
236
|
|
|
{ |
237
|
|
|
if (empty($preg) || empty($str)) { |
238
|
|
|
return null; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
$i = preg_match_all($preg, $str, $matches, PREG_SET_ORDER); |
242
|
|
|
if (0 >= intval($i)) { |
243
|
|
|
// Got none match or Got error |
244
|
|
|
return null; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
// Remove first element of match array, the whole match str part |
248
|
|
View Code Duplication |
foreach ($matches as &$row) { |
|
|
|
|
249
|
|
|
if (1 < count($row)) { |
250
|
|
|
array_shift($row); |
251
|
|
|
} |
252
|
|
|
if (1 == count($row)) { |
253
|
|
|
$row = $row[0]; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
unset($row); |
257
|
|
|
|
258
|
|
|
// Simplify |
259
|
|
|
if (1 == count($matches) && $simple) { |
260
|
|
|
$matches = $matches[0]; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
return $matches; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Match a string with rule including wildcard |
269
|
|
|
* |
270
|
|
|
* Wildcard '*' means any number of chars, and '?' means EXACTLY one char. |
271
|
|
|
* |
272
|
|
|
* Eg: 'duck' match rule '*c?' |
273
|
|
|
* |
274
|
|
|
* @param string $str |
275
|
|
|
* @param string $rule |
276
|
|
|
* @return boolean |
277
|
|
|
*/ |
278
|
|
View Code Duplication |
public function matchWildcard($str, $rule) |
|
|
|
|
279
|
|
|
{ |
280
|
|
|
// Convert wildcard rule to regex |
281
|
|
|
$rule = str_replace('*', '.*', $rule); |
282
|
|
|
$rule = str_replace('?', '.{1}', $rule); |
283
|
|
|
$rule = '/' . $rule . '/'; |
284
|
|
|
|
285
|
|
|
// Must match whole string, same length |
286
|
|
|
if ((1 == preg_match($rule, $str, $matches)) |
287
|
|
|
&& (strlen($matches[0]) == strlen($str)) |
288
|
|
|
) { |
289
|
|
|
return true; |
290
|
|
|
} else { |
291
|
|
|
return false; |
292
|
|
|
} |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Generate random string |
298
|
|
|
* |
299
|
|
|
* In $mode: |
300
|
|
|
* a means include a-z |
301
|
|
|
* A means include A-Z |
302
|
|
|
* 0 means include 0-9 |
303
|
|
|
* |
304
|
|
|
* @param int $len |
305
|
|
|
* @param string $mode |
306
|
|
|
* @return string |
307
|
|
|
*/ |
308
|
|
|
public function random($len, $mode = 'a0') |
309
|
|
|
{ |
310
|
|
|
$str = ''; |
311
|
|
|
if (preg_match('/[a]/', $mode)) { |
312
|
|
|
$str .= 'abcdefghijklmnopqrstuvwxyz'; |
313
|
|
|
} |
314
|
|
|
if (preg_match('/[A]/', $mode)) { |
315
|
|
|
$str .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
316
|
|
|
} |
317
|
|
|
if (preg_match('/[0]/', $mode)) { |
318
|
|
|
$str .= '0123456789'; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
$result = ''; |
322
|
|
|
$strLen = strlen($str); |
323
|
|
|
|
324
|
|
|
// Algorithm |
325
|
|
|
// 1. rand by str length, faster than 2 |
326
|
|
|
// 2. rand then mode by str length |
327
|
|
|
for ($i = 0; $i < $len; $i ++) { |
328
|
|
|
$result .= $str[mt_rand(0, $strLen - 1)]; |
329
|
|
|
} |
330
|
|
|
return $result; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Get substr by display width, and ignore html tag's length |
336
|
|
|
* |
337
|
|
|
* Using mb_strimwidth() |
338
|
|
|
* |
339
|
|
|
* Notice: No consider of html complement, all html tag treat as zero |
340
|
|
|
* length. |
341
|
|
|
* |
342
|
|
|
* Notice: Self close tag need use style as <br />, not <br>, for correct |
343
|
|
|
* html tag depth compute. |
344
|
|
|
* |
345
|
|
|
* @param string $str Source string |
346
|
|
|
* @param int $length Length |
347
|
|
|
* @param string $marker If str length exceed, cut & fill with this |
348
|
|
|
* @param string $encoding Default is utf-8 |
349
|
|
|
* @return string |
350
|
|
|
* @link http://www.fwolf.com/blog/post/133 |
351
|
|
|
*/ |
352
|
|
|
public function substrIgnoreHtml( |
353
|
|
|
$str, |
354
|
|
|
$length, |
355
|
|
|
$marker = '...', |
356
|
|
|
$encoding = 'utf-8' |
357
|
|
|
) { |
358
|
|
|
$str = htmlspecialchars_decode($str); |
359
|
|
|
|
360
|
|
|
$i = preg_match_all('/<[^>]*>/i', $str, $matches); |
361
|
|
|
if (0 == $i) { |
362
|
|
|
// No html in $str |
363
|
|
|
$str = mb_strimwidth($str, 0, $length, $marker, $encoding); |
364
|
|
|
$str = htmlspecialchars($str); |
365
|
|
|
|
366
|
|
|
return $str; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
// Have html tags, need split str into parts by html |
370
|
|
|
$matches = $matches[0]; |
371
|
|
|
|
372
|
|
|
$arParts = []; |
373
|
|
|
foreach ($matches as $match) { |
374
|
|
|
// Find position of match in source string |
375
|
|
|
$pos = strpos($str, $match); |
376
|
|
|
|
377
|
|
|
// Add 2 parts by position |
378
|
|
|
// Part 1 is normal text before matched html tag |
379
|
|
|
$part = substr($str, 0, $pos); |
380
|
|
|
$arParts[] = [ |
381
|
|
|
'content' => $part, |
382
|
|
|
'depth' => 0, |
383
|
|
|
'width' => mb_strwidth($part, $encoding), |
384
|
|
|
]; |
385
|
|
|
|
386
|
|
|
// Part 2 is html tag |
387
|
|
|
if (0 < preg_match('/\/\s*>/', $match)) { |
388
|
|
|
// Self close tag |
389
|
|
|
$depth = 0; |
390
|
|
|
} elseif (0 < preg_match('/<\s*\//', $match)) { |
391
|
|
|
// End tag |
392
|
|
|
$depth = -1; |
393
|
|
|
} else { |
394
|
|
|
$depth = 1; |
395
|
|
|
} |
396
|
|
|
$arParts[] = [ |
397
|
|
|
'content' => $match, |
398
|
|
|
'depth' => $depth, |
399
|
|
|
'width' => 0, |
400
|
|
|
]; |
401
|
|
|
|
402
|
|
|
// Cut source string for next loop |
403
|
|
|
$str = substr($str, $pos + strlen($match)); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
// All left part of source str, after all html tags |
407
|
|
|
$arParts[] = [ |
408
|
|
|
'content' => $str, |
409
|
|
|
'depth' => 0, |
410
|
|
|
'width' => mb_strwidth($str, $encoding), |
411
|
|
|
]; |
412
|
|
|
|
413
|
|
|
// Remove empty parts |
414
|
|
|
$arParts = array_filter($arParts, function ($part) { |
415
|
|
|
return 0 < strlen($part['content']); |
416
|
|
|
}); |
417
|
|
|
|
418
|
|
|
// Loop to cut needed length |
419
|
|
|
$result = ''; |
420
|
|
|
$totalDepth = 0; |
421
|
|
|
foreach ($arParts as $part) { |
422
|
|
|
if (0 >= $length && 0 == $totalDepth) { |
423
|
|
|
break; |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
$width = $part['width']; |
427
|
|
|
if (0 == $width) { |
428
|
|
|
$result .= $part['content']; |
429
|
|
|
$totalDepth += $part['depth']; |
430
|
|
|
|
431
|
|
|
} else { |
432
|
|
|
$result .= htmlspecialchars(mb_strimwidth( |
433
|
|
|
$part['content'], |
434
|
|
|
0, |
435
|
|
|
max($length, 0), |
436
|
|
|
$marker, |
437
|
|
|
$encoding |
438
|
|
|
)); |
439
|
|
|
$length -= $width; |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
return $result; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Convert string to array by splitter |
449
|
|
|
* |
450
|
|
|
* @param string $source |
451
|
|
|
* @param string $splitter |
452
|
|
|
* @param boolean $trim |
453
|
|
|
* @param boolean $removeEmpty |
454
|
|
|
* @return array |
455
|
|
|
*/ |
456
|
|
|
public function toArray( |
457
|
|
|
$source, |
458
|
|
|
$splitter = ',', |
459
|
|
|
$trim = true, |
460
|
|
|
$removeEmpty = true |
461
|
|
|
) { |
462
|
|
|
if (!is_string($source)) { |
463
|
|
|
$source = strval($source); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
$rs = explode($splitter, $source); |
467
|
|
|
|
468
|
|
|
if ($trim) { |
469
|
|
|
foreach ($rs as &$v) { |
470
|
|
|
$v = trim($v); |
471
|
|
|
} |
472
|
|
|
unset($v); |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
if ($removeEmpty) { |
476
|
|
|
foreach ($rs as $k => $v) { |
477
|
|
|
if (empty($v)) { |
478
|
|
|
unset($rs[$k]); |
479
|
|
|
} |
480
|
|
|
} |
481
|
|
|
// Re generate array index |
482
|
|
|
$rs = array_merge($rs, []); |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
return $rs; |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
|
489
|
|
|
/** |
490
|
|
|
* Convert to camelCase |
491
|
|
|
* |
492
|
|
|
* @param string $source |
493
|
|
|
* @return string |
494
|
|
|
*/ |
495
|
|
|
public function toCamelCase($source) |
496
|
|
|
{ |
497
|
|
|
return lcfirst($this->toStudlyCaps($source)); |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* Convert to snake case |
503
|
|
|
* |
504
|
|
|
* @param string $source |
505
|
|
|
* @param string $separator |
506
|
|
|
* @param boolean $ucfirstWords |
507
|
|
|
* @return string |
508
|
|
|
*/ |
509
|
|
|
public function toSnakeCase( |
510
|
|
|
$source, |
511
|
|
|
$separator = '_', |
512
|
|
|
$ucfirstWords = false |
513
|
|
|
) { |
514
|
|
|
// Split to words |
515
|
|
|
$s = preg_replace('/([A-Z])/', ' \1', $source); |
516
|
|
|
|
517
|
|
|
// Remove leading space |
518
|
|
|
$s = trim($s); |
519
|
|
|
|
520
|
|
|
// Merge non-words char and replace by space |
521
|
|
|
$s = preg_replace('/[ _\-\.]+/', ' ', $s); |
522
|
|
|
|
523
|
|
|
if ($ucfirstWords) { |
524
|
|
|
$s = ucwords($s); |
525
|
|
|
} else { |
526
|
|
|
$s = strtolower($s); |
527
|
|
|
} |
528
|
|
|
|
529
|
|
|
// Replace space with separator |
530
|
|
|
$s = str_replace(' ', $separator, $s); |
531
|
|
|
|
532
|
|
|
return $s; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
|
536
|
|
|
/** |
537
|
|
|
* Convert to StudlyCaps |
538
|
|
|
* |
539
|
|
|
* @param string $source |
540
|
|
|
* @return string |
541
|
|
|
*/ |
542
|
|
|
public function toStudlyCaps($source) |
543
|
|
|
{ |
544
|
|
|
return $this->toSnakeCase($source, '', true); |
545
|
|
|
} |
546
|
|
|
} |
547
|
|
|
|
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.