Text   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 599
Duplicated Lines 0 %

Test Coverage

Coverage 42.73%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 201
c 2
b 0
f 0
dl 0
loc 599
ccs 94
cts 220
cp 0.4273
rs 3.04
wmc 67

16 Methods

Rating   Name   Duplication   Size   Complexity  
A compare() 0 21 6
A ensureIsString() 0 8 2
A ensureIsNotEmpty() 0 8 2
A ensureIsNotNull() 0 8 2
A ensureIsNotWhiteSpaces() 0 8 2
A ensureIsValidVarName() 0 10 2
A uuid() 0 19 1
A camelize() 0 4 1
A variable() 0 6 1
A delimit() 0 4 1
A underscore() 0 3 1
A humanize() 0 8 2
B cleanInsert() 0 54 7
A format() 0 31 5
D tokenize() 0 72 18
C insert() 0 58 14

How to fix   Complexity   

Complex Class

Complex classes like Text often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Text, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * PHP: Nelson Martell Library file
5
 *
6
 * Copyright © 2015-2021 Nelson Martell (http://nelson6e65.github.io)
7
 *
8
 * Licensed under The MIT License (MIT)
9
 * For full copyright and license information, please see the LICENSE
10
 * Redistributions of files must retain the above copyright notice.
11
 *
12
 * @copyright 2015-2021 Nelson Martell
13
 * @link      http://nelson6e65.github.io/php_nml/
14
 * @since     0.7.0
15
 * @license   http://www.opensource.org/licenses/mit-license.php The MIT License (MIT)
16
 * */
17
18
namespace NelsonMartell\Extensions;
19
20
use InvalidArgumentException;
21
use NelsonMartell\IComparer;
22
use NelsonMartell\StrictObject;
23
24
use function NelsonMartell\msg;
25
use function NelsonMartell\typeof;
26
27
/**
28
 * Provides extension methods to handle strings.
29
 * This class is based on \Cake\Utility\Text of CakePHP(tm) class.
30
 *
31
 * @since 0.7.0
32
 * @since 1.0.0 Remove `\Cake\Utility\Text` dependency.
33
 * @author Nelson Martell <[email protected]>
34
 * @see \Cake\Utility\Text::insert()
35
 * @link http://book.cakephp.org/3.0/en/core-libraries/text.html
36
 * */
37
class Text implements IComparer
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 231
    public static function format($format, ...$args)
93
    {
94 231
        static $options = [
95 231
            'before' => '{',
96 231
            'after'  => '}',
97 231
        ];
98
99 231
        $originalData = $args;
100
101
        // Make it compatible with named placeholders along numeric ones if passed only 1 array as argument
102 231
        if (count($args) === 1 && is_array($args[0])) {
103 217
            $originalData = $args[0];
104
        }
105
106 231
        $data = [];
107
        // Sanitize values to be convertibles into strings
108 231
        foreach ($originalData as $placeholder => $value) {
109 231
            $valueType = typeof($value);
110
111 231
            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 231
            settype($value, 'string');
119 231
            $data[$placeholder] = $value;
120
        }
121
122 231
        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 147
    public static function ensureIsNotNull($obj)
134
    {
135 147
        if (is_null($obj)) {
136
            $msg = msg('Provided object must not be NULL.');
137
            throw new InvalidArgumentException($msg);
138
        }
139
140 147
        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 147
    public static function ensureIsString($obj)
152
    {
153 147
        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 147
        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 145
    public static function ensureIsValidVarName($string)
210
    {
211 145
        $pattern = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/';
212
213 145
        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 145
        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 22
    public static function compare($left, $right)
238
    {
239 22
        if (is_string($left)) {
240 22
            if (typeof($right)->isCustom()) { // String are minor than classes
241 5
                return -1;
242 18
            } elseif (typeof($right)->canBeString()) {
243 16
                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 231
    public static function insert(string $str, array $data, array $options = []): string
301
    {
302 231
        $defaults = [
303 231
            'before' => ':',
304 231
            'after'  => '',
305 231
            'escape' => '\\',
306 231
            'format' => null,
307 231
            'clean'  => false,
308 231
        ];
309 231
        $options += $defaults;
310 231
        $format   = $options['format'];
311 231
        $data     = $data;
312 231
        if (empty($data)) {
313 14
            return $options['clean'] ? static::cleanInsert($str, $options) : $str;
314
        }
315
316 231
        if (!isset($format)) {
317 231
            $format = sprintf(
318 231
                '/(?<!%s)%s%%s%s/',
319 231
                preg_quote($options['escape'], '/'),
320 231
                str_replace('%', '%%', preg_quote($options['before'], '/')),
321 231
                str_replace('%', '%%', preg_quote($options['after'], '/'))
322 231
            );
323
        }
324
325 231
        if (strpos($str, '?') !== false && is_numeric(key($data))) {
326
            $offset = 0;
327
            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...
Bug introduced by
It seems like $str can also be of type array; however, parameter $haystack of strpos() 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

327
            while (($pos = strpos(/** @scrutinizer ignore-type */ $str, '?', $offset)) !== false) {
Loading history...
328
                $val    = array_shift($data);
329
                $offset = $pos + strlen($val);
330
                $str    = substr_replace($str, $val, $pos, 1);
331
            }
332
333
            return $options['clean'] ? static::cleanInsert($str, $options) : $str;
0 ignored issues
show
Bug introduced by
It seems like $str can also be of type array; however, parameter $str of NelsonMartell\Extensions\Text::cleanInsert() 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

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