Completed
Push — master ( 370eb5...ad8519 )
by Nelson
04:21
created

Text::camelize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * PHP: Nelson Martell Library file
4
 *
5
 * Copyright © 2015-2019 Nelson Martell (http://nelson6e65.github.io)
6
 *
7
 * Licensed under The MIT License (MIT)
8
 * For full copyright and license information, please see the LICENSE
9
 * Redistributions of files must retain the above copyright notice.
10
 *
11
 * @copyright 2015-2019 Nelson Martell
12
 * @link      http://nelson6e65.github.io/php_nml/
13
 * @since     0.7.0
14
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License (MIT)
15
 * */
16
namespace NelsonMartell\Extensions;
17
18
use InvalidArgumentException;
19
20
use NelsonMartell\IComparer;
21
use NelsonMartell\StrictObject;
22
23
use function NelsonMartell\msg;
24
use function NelsonMartell\typeof;
25
26
/**
27
 * Provides extension methods to handle strings.
28
 * This class is based on \Cake\Utility\Text of CakePHP(tm) class.
29
 *
30
 * @since 0.7.0
31
 * @since 1.0.0 Remove `\Cake\Utility\Text` dependency.
32
 * @author Nelson Martell <[email protected]>
33
 * @see \Cake\Utility\Text::insert()
34
 * @link http://book.cakephp.org/3.0/en/core-libraries/text.html
35
 * */
36
class Text implements IComparer
37
{
38
39
    /**
40
     * Replaces format elements in a string with the string representation of an
41
     * object matching the list of arguments specified. You can give as many
42
     * params as you need, or an array with values.
43
     *
44
     * ##Usage
45
     * Using numbers as placeholders (encloses between `{` and `}`), you can get
46
     * the matching string representation of each object given. Use `{0}` for
47
     * the fist object, `{1}` for the second, and so on:
48
     *
49
     * ```php
50
     * $format = '{0} is {1} years old, and have {2} cats.';
51
     * echo Text::format($format, 'Bob', 65, 101); // 'Bob is 65 years old, and have 101 cats.'
52
     * ```
53
     *
54
     * You can also use an array to give objects values:
55
     *
56
     * ```php
57
     * $format = '{0} is {1} years old, and have {2} cats.';
58
     * $data   = ['Bob', 65, 101];
59
     * echo Text::format($format, $data); // 'Bob is 65 years old, and have 101 cats.'
60
     * ```
61
     *
62
     * This is specially useful to be able to use non-numeric placeholders (named placeholders):
63
     *
64
     * ```php
65
     * $format = '{name} is {age} years old, and have {n} cats.';
66
     * $data = ['name' => 'Bob', 'n' => 101, 'age' => 65];
67
     * echo Text::format($format, $data); // 'Bob is 65 years old, and have 101 cats.'
68
     * ```
69
     *
70
     * For numeric placeholders, yo can convert the array into a list of arguments.
71
     *
72
     * ```php
73
     * $format = '{0} is {1} years old, and have {2} cats.';
74
     * $data   = ['Bob', 65, 101];
75
     * echo Text::format($format, ...$data); // 'Bob is 65 years old, and have 101 cats.'
76
     * ```
77
     *
78
     * > Note: If objects are not convertible to string, it will throws and catchable exception
79
     * (`InvalidArgumentException`).
80
     *
81
     * @param string      $format An string containing variable placeholders to be replaced. If you provide name
82
     *   placeholders, you must pass the target array as
83
     * @param array<int, mixed> $args   Object(s) to be replaced into $format placeholders.
84
     *   You can provide one item only of type array for named placeholders replacement. For numeric placeholders, you
85
     *   can still pass the array or convert it into arguments by using the '...' syntax instead.
86
     *
87
     * @return string
88
     * @throws InvalidArgumentException if $format is not an string or placeholder values are not string-convertibles.
89
     * @todo   Implement formatting, like IFormatProvider or something like that.
90
     * @author Nelson Martell <[email protected]>
91
     */
92 230
    public static function format($format, ...$args)
93
    {
94 230
        static $options = [
95
            'before'  => '{',
96
            'after'   => '}',
97
        ];
98
99 230
        $originalData = $args;
100
101
        // Make it compatible with named placeholders along numeric ones if passed only 1 array as argument
102 230
        if (count($args) === 1 && is_array($args[0])) {
103 216
            $originalData = $args[0];
104
        }
105
106 230
        $data = [];
107
        // Sanitize values to be convertibles into strings
108 230
        foreach ($originalData as $placeholder => $value) {
109 230
            $valueType = typeof($value);
110
111 230
            if ($valueType->canBeString() === false) {
112 3
                $msg = 'Value for "{{0}}" placeholder is not convertible to string; "{1}" type given.';
113 3
                throw new InvalidArgumentException(msg($msg, $placeholder, $valueType));
114
            }
115
116
            // This is to work-arround a bug in use of ``asort()`` function in ``Text::insert`` (at v3.2.5)
117
            // without SORT_STRING flag... by forcing value to be string.
118 230
            settype($value, 'string');
119 230
            $data[$placeholder] = $value;
120
        }
121
122 230
        return static::insert($format, $data, $options);
123
    }
124
125
    /**
126
     * Ensures that object given is not null. If is `null`, throws and exception.
127
     *
128
     * @param mixed $obj Object to validate
129
     *
130
     * @return mixed Same object
131
     * @throws InvalidArgumentException if object is `null`.
132
     */
133 146
    public static function ensureIsNotNull($obj)
134
    {
135 146
        if (is_null($obj)) {
136
            $msg = msg('Provided object must not be NULL.');
137
            throw new InvalidArgumentException($msg);
138
        }
139
140 146
        return $obj;
141
    }
142
143
    /**
144
     * Ensures that object given is an string. Else, thows an exception
145
     *
146
     * @param mixed $obj Object to validate.
147
     *
148
     * @return string Same object given, but ensured that is an string.
149
     * @throws InvalidArgumentException if object is not an `string`.
150
     */
151 146
    public static function ensureIsString($obj)
152
    {
153 146
        if (!is_string(static::ensureIsNotNull($obj))) {
154
            $msg = msg('Provided object must to be an string; "{0}" given.', typeof($obj));
155
            throw new InvalidArgumentException($msg);
156
        }
157
158 146
        return $obj;
159
    }
160
161
    /**
162
     * Ensures that given string is not empty.
163
     *
164
     * @param string $string String to validate.
165
     *
166
     * @return string Same string given, but ensured that is not empty.
167
     * @throws InvalidArgumentException if string is null or empty.
168
     */
169
    public static function ensureIsNotEmpty($string)
170
    {
171
        if (static::ensureIsString($string) === '') {
172
            $msg = msg('Provided string must not be empty.');
173
            throw new InvalidArgumentException($msg);
174
        }
175
176
        return $string;
177
    }
178
179
    /**
180
     * Ensures that given string is not empty or whitespaces.
181
     *
182
     * @param string $string String to validate.
183
     *
184
     * @return string Same string given, but ensured that is not whitespaces.
185
     * @throws InvalidArgumentException if object is not an `string`.
186
     * @see    \trim()
187
     */
188
    public static function ensureIsNotWhiteSpaces($string)
189
    {
190
        if (trim(static::ensureIsNotEmpty($string)) === '') {
191
            $msg = msg('Provided string must not be white spaces.');
192
            throw new InvalidArgumentException($msg);
193
        }
194
195
        return $string;
196
    }
197
198
    /**
199
     * Ensures that an string follows the PHP variables naming convention.
200
     *
201
     * @param string $string String to be ensured.
202
     *
203
     * @return string
204
     * @throws InvalidArgumentException if object is not an `string` or do not
205
     *   follows the PHP variables naming convention.
206
     *
207
     * @see PropertyExtension::ensureIsValidName()
208
     */
209 144
    public static function ensureIsValidVarName($string)
210
    {
211 144
        $pattern = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/';
212
213 144
        if (!preg_match($pattern, static::ensureIsString($string))) {
214
            $msg = msg('Provided string do not follows PHP variables naming convention: "{0}".', $string);
215
            throw new InvalidArgumentException($msg);
216
        }
217
218 144
        return $string;
219
    }
220
221
222
    /**
223
     * {@inheritDoc}
224
     *
225
     * This methods is specific for the case when one of them are `string`. In other case, will fallback to
226
     * `Objects::compare()`.` You should use it directly instead of this method as comparation function
227
     * for `usort()`.
228
     *
229
     * @param string|mixed $left
230
     * @param string|mixed $right
231
     *
232
     * @return int|null
233
     *
234
     * @since 1.0.0
235
     * @see Objects::compare()
236
     */
237 23
    public static function compare($left, $right)
238
    {
239 23
        if (is_string($left)) {
240 23
            if (typeof($right)->isCustom()) { // String are minor than classes
241 5
                return -1;
242 19
            } elseif (typeof($right)->canBeString()) {
243 17
                return strnatcmp($left, $right);
244
            } else {
245 2
                return -1;
246
            }
247 6
        } elseif (is_string($right)) {
248 6
            $r = static::compare($right, $left);
249
250 6
            if ($r !== null) {
0 ignored issues
show
introduced by
The condition $r !== null is always true.
Loading history...
251 6
                $r *= -1; // Invert result
252
            }
253
254 6
            return $r;
255
        }
256
257 1
        return Objects::compare($left, $right);
258
    }
259
260
261
262
    // ########################################################################
263
    //
264
    // Methods based on CakePHP Utility (https://github.com/cakephp/utility)
265
    //
266
    // Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
267
    //
268
    // ========================================================================
269
270
271
    // ========================================================================
272
    // Cake\Utility\Text
273
    // ------------------------------------------------------------------------
274
275
276
    /**
277
     * Replaces variable placeholders inside a $str with any given $data. Each key in the $data array
278
     * corresponds to a variable placeholder name in $str.
279
     * Example:
280
     * ```
281
     * Text::insert(':name is :age years old.', ['name' => 'Bob', 'age' => '65']);
282
     * ```
283
     * Returns: Bob is 65 years old.
284
     *
285
     * Available $options are:
286
     *
287
     * - before: The character or string in front of the name of the variable placeholder (Defaults to `:`)
288
     * - after: The character or string after the name of the variable placeholder (Defaults to null)
289
     * - escape: The character or string used to escape the before character / string (Defaults to `\`)
290
     * - format: A regex to use for matching variable placeholders. Default is: `/(?<!\\)\:%s/`
291
     *   (Overwrites before, after, breaks escape / clean)
292
     * - clean: A boolean or array with instructions for Text::cleanInsert
293
     *
294
     * @param string $str A string containing variable placeholders
295
     * @param array $data A key => val array where each key stands for a placeholder variable name
296
     *     to be replaced with val
297
     * @param array $options An array of options, see description above
298
     * @return string
299
     */
300 230
    public static function insert(string $str, array $data, array $options = []): string
301
    {
302
        $defaults = [
303 230
            'before' => ':', 'after' => '', 'escape' => '\\', 'format' => null, 'clean' => false,
304
        ];
305 230
        $options += $defaults;
306 230
        $format   = $options['format'];
307 230
        $data     = $data;
308 230
        if (empty($data)) {
309 13
            return $options['clean'] ? static::cleanInsert($str, $options) : $str;
310
        }
311
312 230
        if (!isset($format)) {
313 230
            $format = sprintf(
314 230
                '/(?<!%s)%s%%s%s/',
315 230
                preg_quote($options['escape'], '/'),
316 230
                str_replace('%', '%%', preg_quote($options['before'], '/')),
317 230
                str_replace('%', '%%', preg_quote($options['after'], '/'))
318
            );
319
        }
320
321 230
        if (strpos($str, '?') !== false && is_numeric(key($data))) {
322
            $offset = 0;
323
            while (($pos = strpos($str, '?', $offset)) !== false) {
0 ignored issues
show
Coding Style introduced by
Variable assignment found within a condition. Did you mean to do a comparison ?
Loading history...
324
                $val    = array_shift($data);
325
                $offset = $pos + strlen($val);
326
                $str    = substr_replace($str, $val, $pos, 1);
327
            }
328
329
            return $options['clean'] ? static::cleanInsert($str, $options) : $str;
330
        }
331
332 230
        $dataKeys = array_keys($data);
333 230
        $hashKeys = array_map('crc32', $dataKeys);
334 230
        $tempData = array_combine($dataKeys, $hashKeys);
335 230
        krsort($tempData);
0 ignored issues
show
Bug introduced by
It seems like $tempData can also be of type false; however, parameter $array of krsort() does only seem to accept array, 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

335
        krsort(/** @scrutinizer ignore-type */ $tempData);
Loading history...
336
337
        /** @var array<string, string> $tempData */
338 230
        foreach ($tempData as $key => $hashVal) {
339 230
            $key = sprintf($format, preg_quote($key, '/'));
340 230
            $str = preg_replace($key, $hashVal, $str);
341
        }
342
        /** @var array<string, mixed> $dataReplacements */
343 230
        $dataReplacements = array_combine($hashKeys, array_values($data));
344 230
        foreach ($dataReplacements as $tmpHash => $tmpValue) {
345 230
            $tmpValue = is_array($tmpValue) ? '' : $tmpValue;
346 230
            $str      = str_replace($tmpHash, $tmpValue, $str);
347
        }
348
349 230
        if (!isset($options['format']) && isset($options['before'])) {
350 230
            $str = str_replace($options['escape'] . $options['before'], $options['before'], $str);
351
        }
352
353 230
        return $options['clean'] ? static::cleanInsert($str, $options) : $str;
354
    }
355
356
    /**
357
     * Cleans up a Text::insert() formatted string with given $options depending on the 'clean' key in
358
     * $options. The default method used is text but html is also available. The goal of this function
359
     * is to replace all whitespace and unneeded markup around placeholders that did not get replaced
360
     * by Text::insert().
361
     *
362
     * @param string $str String to clean.
363
     * @param array $options Options list.
364
     * @return string
365
     * @see \Cake\Utility\Text::insert()
366
     */
367
    public static function cleanInsert(string $str, array $options): string
368
    {
369
        $clean = $options['clean'];
370
        if (!$clean) {
371
            return $str;
372
        }
373
        if ($clean === true) {
374
            $clean = ['method' => 'text'];
375
        }
376
        if (!is_array($clean)) {
377
            $clean = ['method' => $options['clean']];
378
        }
379
        switch ($clean['method']) {
380
            case 'html':
381
                $clean  += [
382
                    'word' => '[\w,.]+',
383
                    'andText' => true,
384
                    'replacement' => '',
385
                ];
386
                $kleenex = sprintf(
387
                    '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i',
388
                    preg_quote($options['before'], '/'),
389
                    $clean['word'],
390
                    preg_quote($options['after'], '/')
391
                );
392
                $str     = preg_replace($kleenex, $clean['replacement'], $str);
393
                if ($clean['andText']) {
394
                    $options['clean'] = ['method' => 'text'];
395
                    $str              = static::cleanInsert($str, $options);
396
                }
397
                break;
398
            case 'text':
399
                $clean += [
400
                    'word' => '[\w,.]+',
401
                    'gap' => '[\s]*(?:(?:and|or)[\s]*)?',
402
                    'replacement' => '',
403
                ];
404
405
                $kleenex = sprintf(
406
                    '/(%s%s%s%s|%s%s%s%s)/',
407
                    preg_quote($options['before'], '/'),
408
                    $clean['word'],
409
                    preg_quote($options['after'], '/'),
410
                    $clean['gap'],
411
                    $clean['gap'],
412
                    preg_quote($options['before'], '/'),
413
                    $clean['word'],
414
                    preg_quote($options['after'], '/')
415
                );
416
                $str     = preg_replace($kleenex, $clean['replacement'], $str);
417
                break;
418
        }
419
420
        return $str;
421
    }
422
423
424
    /**
425
     * Generate a random UUID version 4.
426
     *
427
     * Warning: This method should not be used as a random seed for any cryptographic operations.
428
     * Instead you should use the openssl or mcrypt extensions.
429
     *
430
     * It should also not be used to create identifiers that have security implications, such as
431
     * 'unguessable' URL identifiers. Instead you should use `Security::randomBytes()` for that.
432
     *
433
     * @see https://www.ietf.org/rfc/rfc4122.txt
434
     * @return string RFC 4122 UUID
435
     * @copyright Matt Farina MIT License https://github.com/lootils/uuid/blob/master/LICENSE
436
     * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
437
     *
438
     * @since 1.0.0 Copied from https://github.com/cakephp/utility
439
     */
440
    public static function uuid(): string
441
    {
442
        return sprintf(
443
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
444
            // 32 bits for "time_low"
445
            random_int(0, 65535),
446
            random_int(0, 65535),
447
            // 16 bits for "time_mid"
448
            random_int(0, 65535),
449
            // 12 bits before the 0100 of (version) 4 for "time_hi_and_version"
450
            random_int(0, 4095) | 0x4000,
451
            // 16 bits, 8 bits for "clk_seq_hi_res",
452
            // 8 bits for "clk_seq_low",
453
            // two most significant bits holds zero and one for variant DCE1.1
454
            random_int(0, 0x3fff) | 0x8000,
455
            // 48 bits for "node"
456
            random_int(0, 65535),
457
            random_int(0, 65535),
458
            random_int(0, 65535)
459
        );
460
    }
461
462
463
    /**
464
     * Tokenizes a string using $separator, ignoring any instance of $separator that appears between
465
     * $leftBound and $rightBound.
466
     *
467
     * @param string $data The data to tokenize.
468
     * @param string $separator The token to split the data on.
469
     * @param string $leftBound The left boundary to ignore separators in.
470
     * @param string $rightBound The right boundary to ignore separators in.
471
     * @return string[] Array of tokens in $data.
472
     *
473
     * @since 1.0.0 Copied from https://github.com/cakephp/utility
474
     */
475
    public static function tokenize(
476
        string $data,
477
        string $separator = ',',
478
        string $leftBound = '(',
479
        string $rightBound = ')'
480
    ): array {
481
        if (empty($data)) {
482
            return [];
483
        }
484
485
        $depth   = 0;
486
        $offset  = 0;
487
        $buffer  = '';
488
        $results = [];
489
        $length  = mb_strlen($data);
490
        $open    = false;
491
492
        while ($offset <= $length) {
493
            $tmpOffset = -1;
494
            $offsets   = [
495
                mb_strpos($data, $separator, $offset),
496
                mb_strpos($data, $leftBound, $offset),
497
                mb_strpos($data, $rightBound, $offset),
498
            ];
499
            for ($i = 0; $i < 3; $i++) {
500
                if ($offsets[$i] !== false && ($offsets[$i] < $tmpOffset || $tmpOffset === -1)) {
501
                    $tmpOffset = $offsets[$i];
502
                }
503
            }
504
            if ($tmpOffset !== -1) {
505
                $buffer .= mb_substr($data, $offset, $tmpOffset - $offset);
506
                $char    = mb_substr($data, $tmpOffset, 1);
507
                if (!$depth && $char === $separator) {
508
                    $results[] = $buffer;
509
                    $buffer    = '';
510
                } else {
511
                    $buffer .= $char;
512
                }
513
                if ($leftBound !== $rightBound) {
514
                    if ($char === $leftBound) {
515
                        $depth++;
516
                    }
517
                    if ($char === $rightBound) {
518
                        $depth--;
519
                    }
520
                } else {
521
                    if ($char === $leftBound) {
522
                        if (!$open) {
523
                            $depth++;
524
                            $open = true;
525
                        } else {
526
                            $depth--;
527
                            $open = false;
528
                        }
529
                    }
530
                }
531
                $tmpOffset += 1;
532
                $offset     = $tmpOffset;
533
            } else {
534
                $results[] = $buffer . mb_substr($data, $offset);
535
                $offset    = $length + 1;
536
            }
537
        }
538
        if (empty($results) && !empty($buffer)) {
539
            $results[] = $buffer;
540
        }
541
542
        if (!empty($results)) {
543
            return array_map('trim', $results);
544
        }
545
546
        return [];
547
    }
548
549
    // ========================================================================
550
551
552
    // ========================================================================
553
    // Cake\Utility\Inflector
554
    // ------------------------------------------------------------------------
555
556
    /**
557
     * Returns the input lower_case_delimited_string as a CamelCasedString.
558
     *
559
     * @param string $string String to camelize
560
     * @param string $delimiter the delimiter in the input string
561
     * @return string CamelizedStringLikeThis.
562
     * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms
563
     */
564 23
    public static function camelize(string $string, string $delimiter = '_'): string
565
    {
566 23
            $result = str_replace(' ', '', static::humanize($string, $delimiter));
567 23
        return $result;
568
    }
569
570
571
    /**
572
     * Expects a CamelCasedInputString, and produces a lower_case_delimited_string
573
     *
574
     * @param string $string String to delimit
575
     * @param string $delimiter the character to use as a delimiter
576
     * @return string delimited string
577
     */
578 23
    public static function delimit(string $string, string $delimiter = '_'): string
579
    {
580 23
        $result = mb_strtolower(preg_replace('/(?<=\\w)([A-Z])/', $delimiter . '\\1', $string));
581 23
        return $result;
582
    }
583
584
585
    /**
586
     * Returns the input lower_case_delimited_string as 'A Human Readable String'.
587
     * (Underscores are replaced by spaces and capitalized following words.)
588
     *
589
     * @param string $string String to be humanized
590
     * @param string $delimiter the character to replace with a space
591
     * @return string Human-readable string
592
     * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-human-readable-forms
593
     */
594 23
    public static function humanize(string $string, string $delimiter = '_'): string
595
    {
596 23
        $result = explode(' ', str_replace($delimiter, ' ', $string));
597 23
        foreach ($result as &$word) {
598 23
            $word = mb_strtoupper(mb_substr($word, 0, 1)) . mb_substr($word, 1);
599
        }
600 23
        $result = implode(' ', $result);
601 23
        return $result;
602
    }
603
604
605
    /**
606
     * Returns the input CamelCasedString as an underscored_string.
607
     *
608
     * Also replaces dashes with underscores
609
     *
610
     * @param string $string CamelCasedString to be "underscorized"
611
     * @return string underscore_version of the input string
612
     * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-camelcase-and-under-scored-forms
613
     */
614 23
    public static function underscore(string $string): string
615
    {
616 23
        return static::delimit(str_replace('-', '_', $string), '_');
617
    }
618
619
    /**
620
     * Returns camelBacked version of an underscored string.
621
     *
622
     * @param string $string String to convert.
623
     * @return string in variable form
624
     * @link https://book.cakephp.org/3.0/en/core-libraries/inflector.html#creating-variable-names
625
     */
626 23
    public static function variable(string $string): string
627
    {
628 23
        $camelized = static::camelize(static::underscore($string));
629 23
        $replace   = strtolower(substr($camelized, 0, 1));
630 23
        $result    = $replace . substr($camelized, 1);
631 23
        return $result;
632
    }
633
}
634