Passed
Push — master ( 428f46...c925bb )
by Sebastian
02:32
created

ConvertHelper::isInteger()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 11
rs 10
1
<?php
2
/**
3
 * File containing the {@see AppUtils\ConvertHelper} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage ConvertHelper
7
 * @see AppUtils\ConvertHelper
8
 */
9
10
namespace AppUtils;
11
12
/**
13
 * Static conversion helper class: offers a number of utility methods
14
 * to convert variable types, as well as specialized methods for working
15
 * with specific types like dates.
16
 * 
17
 * @package Application Utils
18
 * @subpackage ConvertHelper
19
 * @author Sebastian Mordziol <[email protected]>
20
 */
21
class ConvertHelper
22
{
23
    const ERROR_NORMALIZETABS_INVALID_PARAMS = 23302;
24
    
25
    const ERROR_MONTHTOSTRING_NOT_VALID_MONTH_NUMBER = 23303;
26
    
27
    const ERROR_CANNOT_NORMALIZE_NON_SCALAR_VALUE = 23304;
28
    
29
    /**
30
     * Normalizes tabs in the specified string by indenting everything
31
     * back to the minimum tab distance. With the second parameter,
32
     * tabs can optionally be converted to spaces as well (recommended
33
     * for HTML output).
34
     *
35
     * @param string $string
36
     * @param boolean $tabs2spaces
37
     * @return string
38
     */
39
    public static function normalizeTabs($string, $tabs2spaces = false)
40
    {
41
        if (!is_string($string)) {
0 ignored issues
show
introduced by
The condition is_string($string) is always true.
Loading history...
42
            throw new ConvertHelper_Exception(
43
                'Invalid parameters',
44
                sprintf(
45
                    'Argument for normalizing tabs is not a string, %1$s given.',
46
                    gettype($string)
47
                ),
48
                self::ERROR_NORMALIZETABS_INVALID_PARAMS
49
            );
50
        }
51
52
        $lines = explode("\n", $string);
53
        $max = 0;
54
        $min = 99999;
55
        foreach ($lines as $line) {
56
            $amount = substr_count($line, "\t");
57
            if ($amount > $max) {
58
                $max = $amount;
59
            }
60
61
            if ($amount > 0 && $amount < $min) {
62
                $min = $amount;
63
            }
64
        }
65
66
        $converted = array();
67
        foreach ($lines as $line) {
68
            $amount = substr_count($line, "\t") - $min;
69
            $line = trim($line);
70
            if ($amount >= 1) {
71
                $line = str_repeat("\t", $amount) . $line;
72
            }
73
74
            $converted[] = $line;
75
        }
76
77
        $string = implode("\n", $converted);
78
        if ($tabs2spaces) {
79
            $string = self::tabs2spaces($string);
80
        }
81
82
        return $string;
83
    }
84
85
    /**
86
     * Converts tabs to spaces in the specified string.
87
     * @param string $string
88
     * @return string
89
     */
90
    public static function tabs2spaces($string)
91
    {
92
        return str_replace("\t", '    ', $string);
93
    }
94
    
95
    /**
96
     * Converts the specified amount of seconds into
97
     * a human readable string split in months, weeks,
98
     * days, hours, minutes and seconds.
99
     *
100
     * @param float $seconds
101
     * @return string
102
     */
103
    public static function time2string($seconds)
104
    {
105
        static $units = null;
106
        if (is_null($units)) {
107
            $units = array(
108
                array(
109
                    'value' => 31 * 7 * 24 * 3600,
110
                    'singular' => t('month'),
111
                    'plural' => t('months')
112
                ),
113
                array(
114
                    'value' => 7 * 24 * 3600,
115
                    'singular' => t('week'),
116
                    'plural' => t('weeks')
117
                ),
118
                array(
119
                    'value' => 24 * 3600,
120
                    'singular' => t('day'),
121
                    'plural' => t('days')
122
                ),
123
                array(
124
                    'value' => 3600,
125
                    'singular' => t('hour'),
126
                    'plural' => t('hours')
127
                ),
128
                array(
129
                    'value' => 60,
130
                    'singular' => t('minute'),
131
                    'plural' => t('minutes')
132
                ),
133
                array(
134
                    'value' => 1,
135
                    'singular' => t('second'),
136
                    'plural' => t('seconds')
137
                )
138
            );
139
        }
140
141
        // specifically handle zero
142
        if ($seconds <= 0) {
143
            return '0 ' . t('seconds');
144
        }
145
        
146
        if($seconds < 1) {
147
            return t('less than a second');
148
        }
149
150
        $tokens = array();
151
        foreach ($units as $def) {
152
            $quot = intval($seconds / $def['value']);
153
            if ($quot) {
154
                $item = $quot . ' ';
155
                if (abs($quot) > 1) {
156
                    $item .= $def['plural'];
157
                } else {
158
                    $item .= $def['singular'];
159
                }
160
161
                $tokens[] = $item;
162
                $seconds -= $quot * $def['value'];
163
            }
164
        }
165
166
        $last = array_pop($tokens);
167
        if (empty($tokens)) {
168
            return $last;
169
        }
170
171
        return implode(', ', $tokens) . ' ' . t('and') . ' ' . $last;
172
    }
173
174
    /**
175
     * Converts a timestamp into an easily understandable
176
     * format, e.g. "2 hours", "1 day", "3 months"
177
     *
178
     * If you set the date to parameter, the difference
179
     * will be calculated between the two dates and not
180
     * the current time.
181
     *
182
     * @param float|\DateTime $datefrom
183
     * @param float|\DateTime $dateto
184
     * @link http://www.sajithmr.com/php-time-ago-calculation/
185
     */
186
    public static function duration2string($datefrom, $dateto = -1)
187
    {
188
        if($datefrom instanceof \DateTime) {
189
            $datefrom = ConvertHelper::date2timestamp($datefrom);
190
        }
191
        
192
        if($dateto instanceof \DateTime) {
193
            $dateto = ConvertHelper::date2timestamp($dateto);
194
        }
195
        
196
        // Defaults and assume if 0 is passed in that
197
        // its an error rather than the epoch
198
199
        if ($datefrom <= 0) {
200
            return t('A long time ago');
201
        }
202
        if ($dateto == -1) {
203
            $dateto = time();
204
        }
205
206
        // Calculate the difference in seconds betweeen
207
        // the two timestamps
208
209
        $difference = $dateto - $datefrom;
210
        $interval = "";
211
        
212
        $future = false;
213
        if($difference < 0) {
214
            $difference = $difference * -1;
215
            $future = true;
216
        }
217
218
        // If difference is less than 60 seconds,
219
        // seconds is a good interval of choice
220
221
        if ($difference < 60) {
222
            $interval = "s";
223
        }
224
225
        // If difference is between 60 seconds and
226
        // 60 minutes, minutes is a good interval
227
        elseif ($difference >= 60 && $difference < 60 * 60) {
228
            $interval = "n";
229
        }
230
231
        // If difference is between 1 hour and 24 hours
232
        // hours is a good interval
233
        elseif ($difference >= 60 * 60 && $difference < 60 * 60 * 24) {
234
            $interval = "h";
235
        }
236
237
        // If difference is between 1 day and 7 days
238
        // days is a good interval
239
        elseif ($difference >= 60 * 60 * 24 && $difference < 60 * 60 * 24 * 7) {
240
            $interval = "d";
241
        }
242
243
        // If difference is between 1 week and 30 days
244
        // weeks is a good interval
245
        elseif ($difference >= 60 * 60 * 24 * 7 && $difference < 60 * 60 * 24 * 30) {
246
            $interval = "ww";
247
        }
248
249
        // If difference is between 30 days and 365 days
250
        // months is a good interval, again, the same thing
251
        // applies, if the 29th February happens to exist
252
        // between your 2 dates, the function will return
253
        // the 'incorrect' value for a day
254
        elseif ($difference >= 60 * 60 * 24 * 30 && $difference < 60 * 60 * 24 * 365) {
255
            $interval = "m";
256
        }
257
258
        // If difference is greater than or equal to 365
259
        // days, return year. This will be incorrect if
260
        // for example, you call the function on the 28th April
261
        // 2008 passing in 29th April 2007. It will return
262
        // 1 year ago when in actual fact (yawn!) not quite
263
        // a year has gone by
264
        elseif ($difference >= 60 * 60 * 24 * 365) {
265
            $interval = "y";
266
        }
267
        
268
        $result = '';
269
270
        // Based on the interval, determine the
271
        // number of units between the two dates
272
        // From this point on, you would be hard
273
        // pushed telling the difference between
274
        // this function and DateDiff. If the $datediff
275
        // returned is 1, be sure to return the singular
276
        // of the unit, e.g. 'day' rather 'days'
277
        switch ($interval) 
278
        {
279
            case "m":
280
                $months_difference = (int)floor($difference / 60 / 60 / 24 / 29);
281
                $hour = (int)date("H", $datefrom);
0 ignored issues
show
Bug introduced by
It seems like $datefrom can also be of type double; however, parameter $timestamp of date() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

281
                $hour = (int)date("H", /** @scrutinizer ignore-type */ $datefrom);
Loading history...
282
                $min = (int)date("i", $datefrom);
283
                $sec = (int)date("s", $datefrom);
284
                $month = (int)date("n", $datefrom);
285
                $day = (int)date("j", $dateto);
286
                $year = (int)date("Y", $datefrom);
287
                
288
                while(mktime($hour, $min, $sec, $month + ($months_difference), $day, $year) < $dateto) 
289
                {
290
                    $months_difference++;
291
                }
292
                
293
                $datediff = $months_difference;
294
295
                // We need this in here because it is possible
296
                // to have an 'm' interval and a months
297
                // difference of 12 because we are using 29 days
298
                // in a month
299
                if ($datediff == 12) {
300
                    $datediff--;
301
                }
302
303
                if($future) {
304
                    $result = ($datediff == 1) ? t('In one month', $datediff) : t('In %1s months', $datediff);
305
                } else {
306
                    $result = ($datediff == 1) ? t('One month ago', $datediff) : t('%1s months ago', $datediff);
307
                }
308
                break;
309
310
            case "y":
311
                $datediff = floor($difference / 60 / 60 / 24 / 365);
312
                if($future) {
313
                    $result = ($datediff == 1) ? t('In one year', $datediff) : t('In %1s years', $datediff);
314
                } else {
315
                    $result = ($datediff == 1) ? t('One year ago', $datediff) : t('%1s years ago', $datediff);
316
                }
317
                break;
318
319
            case "d":
320
                $datediff = floor($difference / 60 / 60 / 24);
321
                if($future) {
322
                    $result = ($datediff == 1) ? t('In one day', $datediff) : t('In %1s days', $datediff);
323
                } else {
324
                    $result = ($datediff == 1) ? t('One day ago', $datediff) : t('%1s days ago', $datediff);
325
                }
326
                break;
327
328
            case "ww":
329
                $datediff = floor($difference / 60 / 60 / 24 / 7);
330
                if($future) {
331
                    $result = ($datediff == 1) ? t('In one week', $datediff) : t('In %1s weeks', $datediff);
332
                } else {
333
                    $result = ($datediff == 1) ? t('One week ago', $datediff) : t('%1s weeks ago', $datediff);
334
                }
335
                break;
336
337
            case "h":
338
                $datediff = floor($difference / 60 / 60);
339
                if($future) {
340
                    $result = ($datediff == 1) ? t('In one hour', $datediff) : t('In %1s hours', $datediff);
341
                } else {
342
                    $result = ($datediff == 1) ? t('One hour ago', $datediff) : t('%1s hours ago', $datediff);
343
                }
344
                break;
345
346
            case "n":
347
                $datediff = floor($difference / 60);
348
                if($future) {
349
                    $result = ($datediff == 1) ? t('In one minute', $datediff) : t('In %1s minutes', $datediff);
350
                } else {
351
                    $result = ($datediff == 1) ? t('One minute ago', $datediff) : t('%1s minutes ago', $datediff);
352
                }
353
                break;
354
355
            case "s":
356
                $datediff = $difference;
357
                if($future) {
358
                    $result = ($datediff == 1) ? t('In one second', $datediff) : t('In %1s seconds', $datediff);
359
                } else {
360
                    $result = ($datediff == 1) ? t('One second ago', $datediff) : t('%1s seconds ago', $datediff);
361
                }
362
                break;
363
        }
364
365
        return $result;
366
    }
367
368
    /**
369
     * Adds syntax highlighting to the specified SQL string in HTML format
370
     * @param string $sql
371
     * @return string
372
     */
373
    public static function highlight_sql($sql)
374
    {
375
        $geshi = new  \GeSHi($sql, 'sql');
376
377
        return $geshi->parse_code();
378
    }
379
    
380
    public static function highlight_xml($xml, $formatSource=false)
381
    {
382
        if($formatSource) 
383
        {
384
            $dom = new \DOMDocument();
385
            $dom->loadXML($xml);
386
            $dom->preserveWhiteSpace = false;
387
            $dom->formatOutput = true;
388
            
389
            $xml = $dom->saveXML();
390
        }
391
        
392
        $geshi = new \GeSHi($xml, 'xml');
393
        
394
        return $geshi->parse_code();
395
    }
396
397
    public static function highlight_php($php)
398
    {
399
        $geshi = new \GeSHi($php, 'php');
400
    
401
        return $geshi->parse_code();
402
    }
403
    
404
    /**
405
     * Converts a number of bytes to a human readable form,
406
     * e.g. xx Kb / xx Mb / xx Gb
407
     *
408
     * @param $bytes
409
     * @param $precision
410
     * @return string
411
     */
412
    public static function bytes2readable($bytes, $precision = 1)
413
    {
414
        $kilobyte = 1024;
415
        $megabyte = $kilobyte * 1024;
416
        $gigabyte = $megabyte * 1024;
417
        $terabyte = $gigabyte * 1024;
418
419
        if (($bytes >= 0) && ($bytes < $kilobyte)) {
420
            return $bytes . ' ' . t('B');
421
422
        } elseif (($bytes >= $kilobyte) && ($bytes < $megabyte)) {
423
            return round($bytes / $kilobyte, $precision) . ' ' . t('Kb');
424
425
        } elseif (($bytes >= $megabyte) && ($bytes < $gigabyte)) {
426
            return round($bytes / $megabyte, $precision) . ' ' . t('Mb');
427
428
        } elseif (($bytes >= $gigabyte) && ($bytes < $terabyte)) {
429
            return round($bytes / $gigabyte, $precision) . ' ' . t('Gb');
430
431
        } elseif ($bytes >= $terabyte) {
432
            return round($bytes / $gigabyte, $precision) . ' ' . t('Tb');
433
        }
434
435
        return $bytes . ' ' . t('B');
436
    }
437
438
   /**
439
    * Cuts a text to the specified length if it is longer than the
440
    * target length. Appends a text to signify it has been cut at 
441
    * the end of the string.
442
    * 
443
    * @param string $text
444
    * @param int $targetLength
445
    * @param string $append
446
    * @return string
447
    */
448
    public static function text_cut(string $text, int $targetLength, string $append = '...') : string
449
    {
450
        $length = mb_strlen($text);
451
        if ($length <= $targetLength) {
452
            return $text;
453
        }
454
455
        $text = trim(mb_substr($text, 0, $targetLength)) . $append;
456
457
        return $text;
458
    }
459
460
    public static function var_dump($var, $html=true)
461
    {
462
        $info = parseVariable($var);
463
        
464
        if($html) {
465
            return $info->toHTML();
466
        }
467
        
468
        return $info->toString();
469
    }
470
    
471
    public static function print_r($var, $return=false, $html=true)
472
    {
473
        $result = self::var_dump($var, $html);
474
        
475
        if($html) {
476
            $result = 
477
            '<pre style="background:#fff;color:#333;padding:16px;border:solid 1px #bbb;border-radius:4px">'.
478
                $result.
479
            '</pre>';
480
        }
481
        
482
        if($return) {
483
            return $result;
484
        }
485
        
486
        echo $result;
487
    }
488
    
489
    protected static $booleanStrings = array(
490
        1 => true,
491
        0 => false,
492
        '1' => true,
493
        '0' => false,
494
        'true' => true,
495
        'false' => false,
496
        'yes' => true,
497
        'no' => false
498
    );
499
500
    public static function string2bool($string)
501
    {
502
        if($string === '' || $string === null) {
503
            return false;
504
        }
505
        
506
        if (is_bool($string)) {
507
            return $string;
508
        }
509
510
        if (!array_key_exists($string, self::$booleanStrings)) {
511
            throw new \InvalidArgumentException('Invalid string boolean representation');
512
        }
513
514
        return self::$booleanStrings[$string];
515
    }
516
    
517
   /**
518
    * Whether the specified string is a boolean string or boolean value.
519
    * Alias for {@link ConvertHelper::isBoolean()}.
520
    * 
521
    * @param mixed $string
522
    * @return bool
523
    * @deprecated
524
    * @see ConvertHelper::isBoolean()
525
    */
526
    public static function isBooleanString($string) : bool
527
    {
528
        return self::isBoolean($string);
529
    }
530
531
   /**
532
    * Alias for the {@\AppUtils\XMLHelper::string2xml()} method.
533
    * 
534
    * @param string $text
535
    * @return string
536
    * @deprecated
537
    */
538
    public static function text_makeXMLCompliant($text)
539
    {
540
        return XMLHelper::string2xml($text);
541
    }
542
543
    /**
544
     * Transforms a date into a generic human readable date, optionally with time.
545
     * If the year is the same as the current one, it is omitted.
546
     *
547
     * - 6 Jan 2012
548
     * - 12 Dec 2012 17:45
549
     * - 5 Aug
550
     *
551
     * @param \DateTime $date
552
     * @return string
553
     */
554
    public static function date2listLabel(\DateTime $date, $includeTime = false, $shortMonth = false)
555
    {
556
        $today = new \DateTime();
557
        if($date->format('d.m.Y') == $today->format('d.m.Y')) {
558
            $label = t('Today');
559
        } else {
560
            $label = $date->format('d') . '. ' . self::month2string((int)$date->format('m'), $shortMonth) . ' ';
561
            if ($date->format('Y') != date('Y')) {
562
                $label .= $date->format('Y');
563
            }
564
        }
565
        
566
        if ($includeTime) {
567
            $label .= $date->format(' H:i');
568
        }
569
570
        return trim($label);
571
    }
572
573
    protected static $months;
574
575
    /**
576
     * Returns a human readable month name given the month number. Can optionally
577
     * return the shorthand version of the month. Translated into the current
578
     * application locale.
579
     *
580
     * @param int|string $monthNr
581
     * @param boolean $short
582
     * @throws ConvertHelper_Exception
583
     * @return string
584
     */
585
    public static function month2string($monthNr, $short = false)
586
    {
587
        if (!isset(self::$months)) {
588
            self::$months = array(
589
                1 => array(t('January'), t('Jan')),
590
                2 => array(t('February'), t('Feb')),
591
                3 => array(t('March'), t('Mar')),
592
                4 => array(t('April'), t('Apr')),
593
                5 => array(t('May'), t('May')),
594
                6 => array(t('June'), t('Jun')),
595
                7 => array(t('July'), t('Jul')),
596
                8 => array(t('August'), t('Aug')),
597
                9 => array(t('September'), t('Sep')),
598
                10 => array(t('October'), t('Oct')),
599
                11 => array(t('November'), t('Nov')),
600
                12 => array(t('December'), t('Dec'))
601
            );
602
        }
603
604
        $monthNr = intval($monthNr);
605
        if (!isset(self::$months[$monthNr])) {
606
            throw new ConvertHelper_Exception(
607
                'Invalid month number',
608
                sprintf('%1$s is not a valid month number.', $monthNr),
609
                self::ERROR_MONTHTOSTRING_NOT_VALID_MONTH_NUMBER
610
            );
611
        }
612
613
        if ($short) {
614
            return self::$months[$monthNr][1];
615
        }
616
617
        return self::$months[$monthNr][0];
618
    }
619
620
    /**
621
     * Transliterates a string.
622
     *
623
     * @param string $string
624
     * @param string $spaceChar
625
     * @param string $lowercase
626
     * @return string
627
     */
628
    public static function transliterate($string, $spaceChar = '-', $lowercase = true)
629
    {
630
        $translit = new Transliteration();
631
        $translit->setSpaceReplacement($spaceChar);
632
        if ($lowercase) {
633
            $translit->setLowercase();
634
        }
635
636
        return $translit->convert($string);
637
    }
638
    
639
   /**
640
    * Retrieves the HEX character codes for all control
641
    * characters that the {@link stripControlCharacters()} 
642
    * method will remove.
643
    * 
644
    * @return string[]
645
    */
646
    public static function getControlCharactersAsHex()
647
    {
648
        $hexAlphabet = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F');
649
        
650
        $stack = array();
651
        foreach(self::$controlChars as $char)
652
        {
653
            $tokens = explode('-', $char);
654
            $start = $tokens[0];
655
            $end = $tokens[1];
656
            $prefix = substr($start, 0, 3);
657
            $range = array();
658
            foreach($hexAlphabet as $number) {
659
                $range[] = $prefix.$number;
660
            }
661
            
662
            $use = false;
663
            foreach($range as $number) {
664
                if($number == $start) {
665
                    $use = true;
666
                }
667
                
668
                if($use) {
669
                    $stack[] = $number;
670
                }
671
                
672
                if($number == $end) {
673
                    break;
674
                }
675
            }
676
        }
677
        
678
        return $stack;
679
    }
680
    
681
   /**
682
    * Retrieves an array of all control characters that
683
    * the {@link stripControlCharacters()} method will 
684
    * remove, as the actual UTF-8 characters.
685
    * 
686
    * @return string[]
687
    */
688
    public static function getControlCharactersAsUTF8()
689
    {
690
        $chars = self::getControlCharactersAsHex();
691
        
692
        $result = array();
693
        foreach($chars as $char) {
694
            $result[] = hex2bin($char);
695
        }
696
        
697
        return $result;
698
    }
699
    
700
   /**
701
    * Retrieves all control characters as JSON encoded
702
    * characters, e.g. "\u200b".
703
    * 
704
    * @return string[]
705
    */
706
    public static function getControlCharactersAsJSON()
707
    {
708
        $chars = self::getControlCharactersAsHex();
709
        
710
        $result = array();
711
        foreach($chars as $char) {
712
            $result[] = '\u'.strtolower($char);
713
        }
714
        
715
        return $result;
716
    }
717
    
718
    protected static $controlChars =  array(
719
        '0000-0008', // control chars
720
        '000E-000F', // control chars
721
        '0010-001F', // control chars
722
        '2000-200F', // non-breaking space and co
723
    );
724
    
725
    protected static $controlCharsRegex;
726
727
    /**
728
     * Removes all control characters from the specified string
729
     * that can cause problems in some cases, like creating
730
     * valid XML documents. This includes invisible non-breaking
731
     * spaces.
732
     *
733
     * @param string $string
734
     * @return string
735
     * @see https://stackoverflow.com/a/8171868/2298192
736
     * @see https://unicode-table.com/en
737
     */
738
    public static function stripControlCharacters(string $string) : string
739
    {
740
        if(empty($string)) {
741
            return $string;
742
        }
743
        
744
        // create the regex from the unicode characters list
745
        if(!isset(self::$controlCharsRegex)) 
746
        {
747
            $chars = self::getControlCharactersAsHex();
748
749
            // we use the notation \x{0000} to specify the unicode character key
750
            // in the regular expression.
751
            $stack = array();
752
            foreach($chars as $char) {
753
                $stack[] = '\x{'.$char.'}';
754
            }
755
            
756
            self::$controlCharsRegex = '/['.implode('', $stack).']/u';
757
        }
758
        
759
        return preg_replace(self::$controlCharsRegex, '', $string);
760
    }
761
762
   /**
763
    * Converts a unicode character to the PHPO notation.
764
    * 
765
    * Example:
766
    * 
767
    * <pre>unicodeChar2php('"\u0000"')</pre>
768
    * 
769
    * Returns
770
    * 
771
    * <pre>\x0</pre>
772
    * 
773
    * @param string $unicodeChar
774
    * @return string
775
    */
776
    public static function unicodeChar2php($unicodeChar) 
777
    {
778
        $unicodeChar = json_decode($unicodeChar);
779
        
780
        /** @author Krinkle 2018 */
781
        $output = '';
782
        foreach (str_split($unicodeChar) as $octet) {
783
            $ordInt = ord($octet);
784
            // Convert from int (base 10) to hex (base 16), for PHP \x syntax
785
            $ordHex = base_convert($ordInt, 10, 16);
786
            $output .= '\x' . $ordHex;
787
        }
788
        return $output;
789
    }
790
    
791
    /**
792
     * Removes the extension from the specified filename
793
     * and returns the name without the extension.
794
     *
795
     * Example:
796
     * filename.html > filename
797
     * passed.test.jpg > passed.test
798
     * path/to/file/document.txt > document
799
     *
800
     * @param string $filename
801
     * @return string
802
     */
803
    public static function filenameRemoveExtension($filename)
804
    {
805
        return FileHelper::removeExtension($filename);
806
    }
807
    
808
    public static function areVariablesEqual($a, $b) : bool
809
    {
810
        $a = self::convertScalarForComparison($a);
811
        $b = self::convertScalarForComparison($b);
812
813
        return $a === $b;
814
    }
815
    
816
    protected static function convertScalarForComparison($scalar)
817
    {
818
        if($scalar === '' || is_null($scalar)) {
819
            return null;
820
        }
821
        
822
        if(is_bool($scalar)) {
823
            return self::bool2string($scalar);
824
        }
825
        
826
        if(is_array($scalar)) {
827
            $scalar = md5(serialize($scalar));
828
        }
829
        
830
        if($scalar !== null && !is_scalar($scalar)) {
831
            throw new ConvertHelper_Exception(
832
                'Not a scalar value in comparison',
833
                null,
834
                self::ERROR_CANNOT_NORMALIZE_NON_SCALAR_VALUE
835
            );
836
        }
837
        
838
        return strval($scalar);
839
    }
840
841
    /**
842
     * Compares two strings to check whether they are equal.
843
     * null and empty strings are considered equal.
844
     *
845
     * @param string $a
846
     * @param string $b
847
     * @return boolean
848
     */
849
    public static function areStringsEqual($a, $b) : bool
850
    {
851
        return self::areVariablesEqual($a, $b);
852
    }
853
854
    /**
855
     * Checks whether the two specified numbers are equal.
856
     * null and empty strings are considered as 0 values.
857
     *
858
     * @param number|string $a
859
     * @param number|string $b
860
     * @return boolean
861
     */
862
    public static function areNumbersEqual($a, $b) : bool
863
    {
864
        return self::areVariablesEqual($a, $b);
865
    }
866
867
    /**
868
     * Converts a boolean value to a string. Defaults to returning
869
     * 'true' or 'false', with the additional parameter it can also
870
     * return the 'yes' and 'no' variants.
871
     *
872
     * @param boolean|string $boolean
873
     * @param boolean $yesno
874
     * @return string
875
     */
876
    public static function bool2string($boolean, bool $yesno = false) : string
877
    {
878
        // allow 'yes', 'true', 'no', 'false' string notations as well
879
        if(!is_bool($boolean)) {
880
            $boolean = self::string2bool($boolean);
881
        }
882
        
883
        if ($boolean) {
884
            if ($yesno) {
885
                return 'yes';
886
            }
887
888
            return 'true';
889
        }
890
891
        if ($yesno) {
892
            return 'no';
893
        }
894
895
        return 'false';
896
    }
897
    
898
   /**
899
    * Converts an associative array with attribute name > value pairs
900
    * to an attribute string that can be used in an HTML tag. Empty 
901
    * attribute values are ignored.
902
    * 
903
    * Example:
904
    * 
905
    * array2attributeString(array(
906
    *     'id' => 45,
907
    *     'href' => 'http://www.mistralys.com'
908
    * ));
909
    * 
910
    * Result:
911
    * 
912
    * id="45" href="http://www.mistralys.com"
913
    * 
914
    * @param array $array
915
    * @return string
916
    */
917
    public static function array2attributeString($array)
918
    {
919
        $tokens = array();
920
        foreach($array as $attr => $value) {
921
            if($value == '' || $value == null) {
922
                continue;
923
            }
924
            
925
            $tokens[] = $attr.'="'.$value.'"';
926
        }
927
        
928
        if(empty($tokens)) {
929
            return '';
930
        }
931
        
932
        return ' '.implode(' ', $tokens);
933
    }
934
    
935
   /**
936
    * Converts a string so it can safely be used in a javascript
937
    * statement in an HTML tag: uses single quotes around the string
938
    * and encodes all special characters as needed.
939
    * 
940
    * @param string $string
941
    * @return string
942
    */
943
    public static function string2attributeJS($string, $quoted=true)
944
    {
945
        $converted = addslashes(htmlspecialchars(strip_tags($string), ENT_QUOTES, 'UTF-8'));
946
        if($quoted) {
947
            $converted = "'".$converted."'";
948
        } 
949
        
950
        return $converted;
951
    }
952
    
953
   /**
954
    * Checks if the specified string is a boolean value, which
955
    * includes string representations of boolean values, like 
956
    * <code>yes</code> or <code>no</code>, and <code>true</code>
957
    * or <code>false</code>.
958
    * 
959
    * @param mixed $value
960
    * @return boolean
961
    */
962
    public static function isBoolean($value) : bool
963
    {
964
        if(is_bool($value)) {
965
            return true;
966
        }
967
        
968
        if(!is_scalar($value)) {
969
            return false;
970
        }
971
        
972
        return array_key_exists($value, self::$booleanStrings);
973
    }
974
    
975
   /**
976
    * Converts an associative array to an HTML style attribute value string.
977
    * 
978
    * @param array $subject
979
    * @return string
980
    */
981
    public static function array2styleString(array $subject) : string
982
    {
983
        $tokens = array();
984
        foreach($subject as $name => $value) {
985
            $tokens[] = $name.':'.$value;
986
        }
987
        
988
        return implode(';', $tokens);
989
    }
990
    
991
   /**
992
    * Converts a DateTime object to a timestamp, which
993
    * is PHP 5.2 compatible.
994
    * 
995
    * @param \DateTime $date
996
    * @return integer
997
    */
998
    public static function date2timestamp(\DateTime $date) : int
999
    {
1000
        return (int)$date->format('U');
1001
    }
1002
    
1003
   /**
1004
    * Converts a timestamp into a DateTime instance.
1005
    * @param int $timestamp
1006
    * @return \DateTime
1007
    */
1008
    public static function timestamp2date(int $timestamp) : \DateTime
1009
    {
1010
        $date = new \DateTime();
1011
        $date->setTimestamp($timestamp);
1012
        return $date;
1013
    }
1014
    
1015
   /**
1016
    * Strips an absolute path to a file within the application
1017
    * to make the path relative to the application root path.
1018
    * 
1019
    * @param string $path
1020
    * @return string
1021
    */
1022
    public static function fileRelativize($path)
1023
    {
1024
        $path = str_replace('\\', '/', $path);
1025
        $root = str_Replace('\\', '/', APP_ROOT);
0 ignored issues
show
Bug introduced by
The constant AppUtils\APP_ROOT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1026
        return str_replace($root, '', $path);
1027
    }
1028
    
1029
    const JS_REGEX_OBJECT = 'object';
1030
    
1031
    const JS_REGEX_JSON = 'json';
1032
    
1033
   /**
1034
    * Takes a regular expression and attempts to convert it to
1035
    * its javascript equivalent. Returns an array containing the
1036
    * format string itself (without start and end characters),
1037
    * and the modifiers.
1038
    *  
1039
    * This is intended to be used with the RegExp object, for ex:
1040
    * 
1041
    * <script>
1042
    * var expression = <?php echo json_encode(ConvertHelper::regex2js('/ab+c/i')) ?>;
1043
    * var reg = new RegExp(expression.format, expression.modifiers);
1044
    * </script>
1045
    *  
1046
    * @param string $regex
1047
    * @return array
1048
    */
1049
    public static function regex2js($regex, $return=self::JS_REGEX_OBJECT)
1050
    {
1051
        $regex = trim($regex);
1052
        $separator = substr($regex, 0, 1);
1053
        $parts = explode($separator, $regex);
1054
        array_shift($parts);
1055
        
1056
        $modifiers = array_pop($parts);
1057
        if($modifiers == $separator) {
1058
            $modifiers = '';
1059
        }
1060
        
1061
        $modifierReplacements = array(
1062
            's' => '',
1063
            'U' => ''
1064
        );
1065
        
1066
        $modifiers = str_replace(array_keys($modifierReplacements), array_values($modifierReplacements), $modifiers);
1067
        
1068
        $format = implode($separator, $parts);
1069
        
1070
        // convert the anchors that are not supported in js regexes
1071
        $format = str_replace(array('\\A', '\\Z', '\\z'), array('^', '$', ''), $format);
1072
        
1073
        if($return==self::JS_REGEX_JSON) {
1074
            return json_encode(array(
0 ignored issues
show
Bug Best Practice introduced by
The expression return json_encode(array...ifiers' => $modifiers)) returns the type string which is incompatible with the documented return type array.
Loading history...
1075
                'format' => $format,
1076
                'modifiers' => $modifiers
1077
            ));
1078
        }
1079
        
1080
        if(!empty($modifiers)) {
1081
            return sprintf(
0 ignored issues
show
Bug Best Practice introduced by
The expression return sprintf('new RegE...son_encode($modifiers)) returns the type string which is incompatible with the documented return type array.
Loading history...
1082
                'new RegExp(%s, %s)',
1083
                json_encode($format),
1084
                json_encode($modifiers)
1085
            );
1086
        }
1087
        
1088
        return sprintf(
0 ignored issues
show
Bug Best Practice introduced by
The expression return sprintf('new RegE..., json_encode($format)) returns the type string which is incompatible with the documented return type array.
Loading history...
1089
            'new RegExp(%s)',
1090
            json_encode($format)
1091
        );
1092
    }
1093
    
1094
   /**
1095
    * Strips all known UTF byte order marks from the specified string.
1096
    * 
1097
    * @param string $string
1098
    * @return string
1099
    */
1100
    public static function stripUTFBom($string)
1101
    {
1102
        $boms = FileHelper::getUTFBOMs();
1103
        foreach($boms as $bomChars) {
1104
            $length = mb_strlen($bomChars);
1105
            $text = mb_substr($string, 0, $length);
1106
            if($text==$bomChars) {
1107
                return mb_substr($string, $length);
1108
            }
1109
        }
1110
        
1111
        return $string;
1112
    }
1113
1114
   /**
1115
    * Converts a string to valid utf8, regardless
1116
    * of the string's encoding(s).
1117
    * 
1118
    * @param string $string
1119
    * @return string
1120
    */
1121
    public static function string2utf8($string)
1122
    {
1123
        if(!self::isStringASCII($string)) {
1124
            return \ForceUTF8\Encoding::toUTF8($string);
1125
        }
1126
        
1127
        return $string;
1128
    }
1129
    
1130
   /**
1131
    * Checks whether the specified string is an ASCII
1132
    * string, without any special or UTF8 characters.
1133
    * Note: empty strings and NULL are considered ASCII.
1134
    * Any variable types other than strings are not.
1135
    * 
1136
    * @param string $string
1137
    * @return boolean
1138
    */
1139
    public static function isStringASCII($string)
1140
    {
1141
        if($string === '' || $string === NULL) {
1142
            return true;
1143
        }
1144
        
1145
        if(!is_string($string)) {
0 ignored issues
show
introduced by
The condition is_string($string) is always true.
Loading history...
1146
            return false;
1147
        }
1148
        
1149
        return !preg_match('/[^\x00-\x7F]/', $string);
1150
    }
1151
    
1152
    public static function highlight_url($url)
1153
    {
1154
        $url = htmlspecialchars($url);
1155
        $url = str_replace(
1156
            array('/', '='), 
1157
            array('/<wbr>', '=<wbr>'), 
1158
            $url
1159
        );
1160
        return $url;
1161
    }
1162
1163
   /**
1164
    * Calculates a percentage match of the source string with the target string.
1165
    * 
1166
    * Options are:
1167
    * 
1168
    * - maxLevenshtein, default: 10
1169
    *   Any levenshtein results above this value are ignored.
1170
    *   
1171
    * - precision, default: 1
1172
    *   The precision of the percentage float value
1173
    * 
1174
    * @param string $source
1175
    * @param string $target
1176
    * @param array $options
1177
    * @return float
1178
    */
1179
    public static function matchString($source, $target, $options=array())
1180
    {
1181
        $defaults = array(
1182
            'maxLevenshtein' => 10,
1183
            'precision' => 1
1184
        );
1185
        
1186
        $options = array_merge($defaults, $options);
1187
        
1188
        // avoid doing this via levenshtein
1189
        if($source == $target) {
1190
            return 100;
1191
        }
1192
        
1193
        $diff = levenshtein($source, $target);
1194
        if($diff > $options['maxLevenshtein']) {
1195
            return 0;
1196
        }
1197
        
1198
        $percent = $diff * 100 / ($options['maxLevenshtein'] + 1);
1199
        return round(100 - $percent, $options['precision']);
1200
    }
1201
    
1202
    public static function interval2string(\DateInterval $interval)
1203
    {
1204
        $tokens = array('y', 'm', 'd', 'h', 'i', 's');
1205
        
1206
        $offset = 0;
1207
        $keep = array();
1208
        foreach($tokens as $token) {
1209
            if($interval->$token > 0) {
1210
                $keep = array_slice($tokens, $offset);
1211
                break;
1212
            }
1213
            
1214
            $offset++;
1215
        }
1216
        
1217
        $parts = array();
1218
        foreach($keep as $token) 
1219
        {
1220
            $value = $interval->$token;
1221
            $label = '';
1222
            
1223
            $suffix = 'p';
1224
            if($value == 1) { $suffix = 's'; }
1225
            $token .= $suffix;
1226
            
1227
            switch($token) {
1228
                case 'ys': $label = t('1 year'); break;
1229
                case 'yp': $label = t('%1$s years', $value); break;
1230
                case 'ms': $label = t('1 month'); break;
1231
                case 'mp': $label = t('%1$s months', $value); break;
1232
                case 'ds': $label = t('1 day'); break;
1233
                case 'dp': $label = t('%1$s days', $value); break;
1234
                case 'hs': $label = t('1 hour'); break;
1235
                case 'hp': $label = t('%1$s hours', $value); break;
1236
                case 'is': $label = t('1 minute'); break;
1237
                case 'ip': $label = t('%1$s minutes', $value); break;
1238
                case 'ss': $label = t('1 second'); break;
1239
                case 'sp': $label = t('%1$s seconds', $value); break;
1240
            }
1241
            
1242
            $parts[] = $label;
1243
        }
1244
        
1245
        if(count($parts) == 1) {
1246
            return $parts[0];
1247
        } 
1248
        
1249
        $last = array_pop($parts);
1250
        
1251
        return t('%1$s and %2$s', implode(', ', $parts), $last);
1252
    }
1253
    
1254
    const INTERVAL_DAYS = 'days';
1255
    
1256
    const INTERVAL_HOURS = 'hours';
1257
    
1258
    const INTERVAL_MINUTES = 'minutes';
1259
    
1260
    const INTERVAL_SECONDS = 'seconds';
1261
    
1262
   /**
1263
    * Calculates the total amount of days / hours / minutes or seconds
1264
    * of a date interval object and returns the value.
1265
    * 
1266
    * @param \DateInterval $interval
1267
    * @param string $unit
1268
    * @return integer
1269
    */
1270
    public static function interval2total(\DateInterval $interval, $unit=self::INTERVAL_SECONDS)
1271
    {
1272
        $total = $interval->format('%a');
1273
        if ($unit == self::INTERVAL_DAYS) {
1274
            return $total;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $total returns the type string which is incompatible with the documented return type integer.
Loading history...
1275
        }
1276
1277
        $total = ($total * 24) + ($interval->h );
1278
        if ($unit == self::INTERVAL_HOURS) {
1279
            return $total;
1280
        }
1281
    
1282
        $total = ($total * 60) + ($interval->i );
1283
        if ($unit == self::INTERVAL_MINUTES)
1284
            return $total;
1285
1286
        $total = ($total * 60) + ($interval->s );
1287
        if ($unit == self::INTERVAL_SECONDS)
1288
            return $total;
1289
        
1290
        return 0;
1291
    }
1292
1293
    protected static $days;
1294
    
1295
    protected static $daysShort;
1296
1297
    protected static $daysInvariant = array(
1298
        'Monday',
1299
        'Tuesday',
1300
        'Wednesday',
1301
        'Thursday',
1302
        'Friday',
1303
        'Saturday',
1304
        'Sunday'
1305
    );
1306
    
1307
   /**
1308
    * Converts a date to the corresponding day name.
1309
    * 
1310
    * @param \DateTime $date
1311
    * @param string $short
1312
    * @return string|NULL
1313
    */
1314
    public static function date2dayName(\DateTime $date, $short=false)
1315
    {
1316
        $day = $date->format('l');
1317
        $invariant = self::getDayNamesInvariant();
1318
        
1319
        $idx = array_search($day, $invariant);
1320
        if($idx !== false) {
1321
            $localized = self::getDayNames($short);
0 ignored issues
show
Bug introduced by
It seems like $short can also be of type false; however, parameter $short of AppUtils\ConvertHelper::getDayNames() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1321
            $localized = self::getDayNames(/** @scrutinizer ignore-type */ $short);
Loading history...
1322
            return $localized[$idx];
1323
        }
1324
        
1325
        return null;
1326
    }
1327
    
1328
   /**
1329
    * Retrieves a list of english day names.
1330
    * @return string[]
1331
    */
1332
    public static function getDayNamesInvariant()
1333
    {
1334
        return self::$daysInvariant;
1335
    }
1336
    
1337
   /**
1338
    * Retrieves the day names list for the current locale.
1339
    * 
1340
    * @param string $short
1341
    * @return string[]
1342
    */
1343
    public static function getDayNames($short=false)
1344
    {
1345
        if($short) {
1346
            if(!isset(self::$daysShort)) {
1347
                self::$daysShort = array(
1348
                    t('Mon'),
1349
                    t('Tue'),
1350
                    t('Wed'),
1351
                    t('Thu'),
1352
                    t('Fri'),
1353
                    t('Sat'),
1354
                    t('Sun')
1355
                );
1356
            }
1357
            
1358
            return self::$daysShort;
1359
        }
1360
        
1361
        if(!isset(self::$days)) {
1362
            self::$days = array(
1363
                t('Monday'),
1364
                t('Tuesday'),
1365
                t('Wednesday'),
1366
                t('Thursday'),
1367
                t('Friday'),
1368
                t('Saturday'),
1369
                t('Sunday')
1370
            );
1371
        }
1372
        
1373
        return self::$days;
1374
    }
1375
1376
    /**
1377
     * Implodes an array with a separator character, and the last item with "add".
1378
     * 
1379
     * @param array $list The indexed array with items to implode.
1380
     * @param string $sep The separator character to use.
1381
     * @param string $conjunction The word to use as conjunction with the last item in the list. NOTE: include spaces as needed.
1382
     * @return string
1383
     */
1384
    public static function implodeWithAnd(array $list, $sep = ', ', $conjunction = null)
1385
    {
1386
        if(empty($list)) {
1387
            return '';
1388
        }
1389
        
1390
        if(empty($conjunction)) {
1391
            $conjunction = t('and');
1392
        }
1393
        
1394
        $last = array_pop($list);
1395
        if($list) {
1396
            return implode($sep, $list) . $conjunction . ' ' . $last;
1397
        }
1398
        
1399
        return $last;
1400
    }
1401
    
1402
   /**
1403
    * Splits a string into an array of all characters it is composed of.
1404
    * Unicode character safe.
1405
    * 
1406
    * NOTE: Spaces and newlines (both \r and \n) are also considered single
1407
    * characters.
1408
    * 
1409
    * @param string $string
1410
    * @return array
1411
    */
1412
    public static function string2array(string $string) : array
1413
    {
1414
        $result = preg_split('//u', $string, null, PREG_SPLIT_NO_EMPTY);
1415
        if($result !== false) {
1416
            return $result;
1417
        }
1418
        
1419
        return array();
1420
    }
1421
    
1422
   /**
1423
    * Checks whether the specified string contains HTML code.
1424
    * 
1425
    * @param string $string
1426
    * @return boolean
1427
    */
1428
    public static function isStringHTML(string $string) : bool
1429
    {
1430
        if(preg_match('%<[a-z/][\s\S]*>%siU', $string)) {
1431
            return true;
1432
        }
1433
        
1434
        $decoded = html_entity_decode($string);
1435
        if($decoded !== $string) {
1436
            return true;
1437
        }
1438
        
1439
        return false;
1440
    }
1441
    
1442
   /**
1443
    * UTF8-safe wordwrap method: works like the regular wordwrap
1444
    * PHP function but compatible with UTF8. Otherwise the lengths
1445
    * are no calculated correctly.
1446
    * 
1447
    * @param string $str
1448
    * @param int $width
1449
    * @param string $break
1450
    * @param bool $cut
1451
    * @return string
1452
    * @see https://stackoverflow.com/a/4988494/2298192
1453
    */
1454
    public static function wordwrap($str, $width = 75, $break = "\n", $cut = false) 
1455
    {
1456
        $lines = explode($break, $str);
1457
        
1458
        foreach ($lines as &$line) 
1459
        {
1460
            $line = rtrim($line);
1461
            if (mb_strlen($line) <= $width) {
1462
                continue;
1463
            }
1464
        
1465
            $words = explode(' ', $line);
1466
            $line = '';
1467
            $actual = '';
1468
            foreach ($words as $word) 
1469
            {
1470
                if (mb_strlen($actual.$word) <= $width) 
1471
                {
1472
                    $actual .= $word.' ';
1473
                } 
1474
                else 
1475
                {
1476
                    if ($actual != '') {
1477
                        $line .= rtrim($actual).$break;
1478
                    }
1479
                    
1480
                    $actual = $word;
1481
                    if ($cut) 
1482
                    {
1483
                        while (mb_strlen($actual) > $width) {
1484
                            $line .= mb_substr($actual, 0, $width).$break;
1485
                            $actual = mb_substr($actual, $width);
1486
                        }
1487
                    }
1488
                    
1489
                    $actual .= ' ';
1490
                }
1491
            }
1492
            
1493
            $line .= trim($actual);
1494
        }
1495
        
1496
        return implode($break, $lines);
1497
    }
1498
    
1499
   /**
1500
    * Calculates the byte length of a string, taking into 
1501
    * account any unicode characters.
1502
    * 
1503
    * @param string $string
1504
    * @return int
1505
    * @see https://stackoverflow.com/a/9718273/2298192
1506
    */
1507
    public static function string2bytes($string)
1508
    {
1509
        return mb_strlen($string, '8bit');
1510
    }
1511
    
1512
   /**
1513
    * Creates a short, 8-character long hash for the specified string.
1514
    * 
1515
    * WARNING: Not cryptographically safe.
1516
    * 
1517
    * @param string $string
1518
    * @return string
1519
    */
1520
    public static function string2shortHash($string)
1521
    {
1522
        return hash('crc32', $string, false);
1523
    }
1524
    
1525
    public static function string2hash($string)
1526
    {
1527
        return md5($string);
1528
    }
1529
    
1530
    public static function callback2string($callback) : string
1531
    {
1532
        return parseVariable($callback)->toString();
1533
    }
1534
1535
    public static function exception2info(\Throwable $e) : ConvertHelper_ThrowableInfo
1536
    {
1537
        return self::throwable2info($e);
1538
    }
1539
    
1540
    public static function throwable2info(\Throwable $e) : ConvertHelper_ThrowableInfo
1541
    {
1542
        return ConvertHelper_ThrowableInfo::fromThrowable($e);
1543
    }
1544
    
1545
   /**
1546
    * Parses the specified query string like the native 
1547
    * function <code>parse_str</code>, without the key
1548
    * naming limitations.
1549
    * 
1550
    * Using parse_str, dots or spaces in key names are 
1551
    * replaced by underscores. This method keeps all names
1552
    * intact.
1553
    * 
1554
    * It still uses the parse_str implementation as it 
1555
    * is tested and tried, but fixes the parameter names
1556
    * after parsing, as needed.
1557
    * 
1558
    * @param string $queryString
1559
    * @return array
1560
    * @see https://www.php.net/manual/en/function.parse-str.php
1561
    */
1562
    public static function parseQueryString(string $queryString) : array
1563
    {
1564
        // allow HTML entities notation
1565
        $queryString = str_replace('&amp;', '&', $queryString);
1566
        
1567
        $paramNames = array();
1568
        
1569
        // extract parameter names from the query string
1570
        $result = array();
1571
        preg_match_all('/&?([^&]+)=.*/sixU', $queryString, $result, PREG_PATTERN_ORDER);
1572
        if(isset($result[1])) {
1573
            $paramNames = $result[1];
1574
        }
1575
        
1576
        // to avoid iterating over the param names, we simply concatenate it
1577
        $search = implode('', $paramNames);
1578
        
1579
        // store whether we need to adjust any of the names: 
1580
        // this is true if we find dots or spaces in any of them.
1581
        $fixRequired = stristr($search, '.') || stristr($search, ' ');
1582
1583
        unset($search);
1584
        
1585
        $table = array();
1586
        
1587
        // A fix is required: replace all parameter names with placeholders,
1588
        // which do not conflict with parse_str and which will be restored
1589
        // with the actual parameter names after the parsing.
1590
        //
1591
        // It is necessary to do this even before the parsing, to resolve
1592
        // possible naming conflicts like having both parameters "foo.bar" 
1593
        // and "foo_bar" in the query string: since "foo.bar" would be converted
1594
        // to "foo_bar", one of the two would be replaced.
1595
        if($fixRequired) 
1596
        {
1597
            $counter = 1;
1598
            $placeholders = array();
1599
            foreach($paramNames as $paramName)
1600
            {
1601
                 // create a unique placeholder name
1602
                 $placeholder = '__PLACEHOLDER'.$counter.'__';
1603
                 
1604
                 // store the placeholder name to replace later
1605
                 $table[$placeholder] = $paramName;
1606
                 
1607
                 // add the placeholder to replace in the query string before parsing
1608
                 $placeholders[$paramName.'='] = $placeholder.'=';
1609
                 
1610
                 $counter++;
1611
            }
1612
            
1613
            // next challenge: replacing the parameter names by placeholders
1614
            // safely. We sort the list by longest name first, to avoid shorter 
1615
            // parameter names being replaced first that can be part of longer ones.
1616
            uksort($placeholders, function($a, $b) {
1617
                return strlen($b) - strlen($a);
1618
            });
1619
            
1620
            // replace all instances with the placeholder
1621
            $queryString = str_replace(array_keys($placeholders), array_values($placeholders), $queryString);
1622
        }
1623
        
1624
        // parse the query string natively
1625
        $parsed = array();
1626
        parse_str($queryString, $parsed);
1627
        
1628
        // do any of the parameter names need to be fixed?
1629
        if(!$fixRequired) {
1630
            return $parsed;
1631
        }
1632
        
1633
        $keep = array();
1634
        
1635
        foreach($parsed as $name => $value)
1636
        {
1637
             $keep[$table[$name]] = $value;
1638
        }
1639
        
1640
        return $keep;
1641
    }
1642
1643
   /**
1644
    * Searches for needle in the specified string, and returns a list
1645
    * of all occurrences, including the matched string. The matched 
1646
    * string is useful when doing a case insensitive search, as it 
1647
    * shows the exact matched case of needle.
1648
    *   
1649
    * @param string $needle
1650
    * @param string $haystack
1651
    * @param bool $caseInsensitive
1652
    * @return ConvertHelper_StringMatch[]
1653
    */
1654
    public static function findString(string $needle, string $haystack, bool $caseInsensitive=false)
1655
    {
1656
        if($needle === '') {
1657
            return array();
1658
        }
1659
        
1660
        $function = 'mb_strpos';
1661
        if($caseInsensitive) {
1662
            $function = 'mb_stripos';
1663
        }
1664
        
1665
        $pos = 0;
1666
        $positions = array();
1667
        $length = mb_strlen($needle);
1668
        
1669
        while( ($pos = $function($haystack, $needle, $pos)) !== false) 
1670
        {
1671
            $match = mb_substr($haystack, $pos, $length);
1672
            $positions[] = new ConvertHelper_StringMatch($pos, $match);
1673
            $pos += $length;
1674
        }
1675
        
1676
        return $positions;
1677
    }
1678
    
1679
   /**
1680
    * Like explode, but trims all entries, and removes 
1681
    * empty entries from the resulting array.
1682
    * 
1683
    * @param string $delimiter
1684
    * @param string $string
1685
    * @return string[]
1686
    */
1687
    public static function explodeTrim(string $delimiter, string $string) : array
1688
    {
1689
        if(empty($string) || empty($delimiter)) {
1690
            return array();
1691
        }
1692
        
1693
        $tokens = explode($delimiter, $string);
1694
        $tokens = array_map('trim', $tokens);
1695
        
1696
        $keep = array();
1697
        foreach($tokens as $token) {
1698
            if($token !== '') {
1699
                $keep[] = $token;
1700
            }
1701
        }
1702
        
1703
        return $keep;
1704
    }
1705
    
1706
    protected static $eolChars;
1707
1708
   /**
1709
    * Detects the most used end-of-line character in the subject string.
1710
    * 
1711
    * @param string $str The string to check.
1712
    * @return NULL|ConvertHelper_EOL The detected EOL instance, or NULL if none has been detected.
1713
    */
1714
    public static function detectEOLCharacter(string $subjectString) : ?ConvertHelper_EOL
1715
    {
1716
        if(empty($subjectString)) {
1717
            return null;
1718
        }
1719
        
1720
        if(!isset(self::$eolChars))
1721
        {
1722
            $cr = chr((int)hexdec('0d'));
1723
            $lf = chr((int)hexdec('0a'));
1724
            
1725
           self::$eolChars = array(
1726
               array(
1727
                   'char' => $cr.$lf,
1728
                   'type' => ConvertHelper_EOL::TYPE_CRLF,
1729
                   'description' => t('Carriage return followed by a line feed'),
1730
               ),
1731
               array(
1732
                   'char' => $lf.$cr,
1733
                   'type' => ConvertHelper_EOL::TYPE_LFCR,
1734
                   'description' => t('Line feed followed by a carriage return'),
1735
               ),
1736
               array(
1737
                  'char' => $lf,
1738
                  'type' => ConvertHelper_EOL::TYPE_LF,
1739
                  'description' => t('Line feed'),
1740
               ),
1741
               array(
1742
                  'char' => $cr,
1743
                  'type' => ConvertHelper_EOL::TYPE_CR,
1744
                  'description' => t('Carriage Return'),
1745
               ),
1746
            );
1747
        }
1748
        
1749
        $max = 0;
1750
        $results = array();
1751
        foreach(self::$eolChars as $def) 
1752
        {
1753
            $amount = substr_count($subjectString, $def['char']);
1754
            
1755
            if($amount > $max)
1756
            {
1757
                $max = $amount;
1758
                $results[] = $def;
1759
            }
1760
        }
1761
        
1762
        if(empty($results)) {
1763
            return null;
1764
        }
1765
        
1766
        return new ConvertHelper_EOL(
1767
            $results[0]['char'], 
1768
            $results[0]['type'],
1769
            $results[0]['description']
1770
        );
1771
    }
1772
1773
   /**
1774
    * Removes the specified keys from the target array,
1775
    * if they exist.
1776
    * 
1777
    * @param array $array
1778
    * @param array $keys
1779
    */
1780
    public static function arrayRemoveKeys(array &$array, array $keys) : void
1781
    {
1782
        foreach($keys as $key) 
1783
        {
1784
            if(array_key_exists($key, $array)) {
1785
                unset($array[$key]); 
1786
            }
1787
        }
1788
    }
1789
    
1790
   /**
1791
    * Checks if the specified variable is an integer or a string containing an integer.
1792
    * @param mixed $value
1793
    * @return bool
1794
    */
1795
    public static function isInteger($value) : bool
1796
    {
1797
        if(is_int($value)) {
1798
            return true;
1799
        }
1800
        
1801
        if(is_string($value)) {
1802
            return preg_match('/\A\d+\z/', $value);
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_match('/\A\d+\z/', $value) returns the type integer which is incompatible with the type-hinted return boolean.
Loading history...
1803
        }
1804
        
1805
        return false;    
1806
    }
1807
}
1808