StringUtil::substrIgnoreHtml()   C
last analyzed

Complexity

Conditions 9
Paths 9

Size

Total Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 9
nop 4
dl 0
loc 93
rs 6.5971
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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
                '  '    => '&nbsp; ',
68
                ' '     => '&nbsp;',
69
                '&nbsp;&nbsp;'  => '&nbsp; ',
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) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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