Passed
Push — master ( 6c6d7e...b48c4f )
by Marwan
01:39
created

Misc::setArrayValueByKey()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 24
ccs 13
cts 13
cp 1
crap 5
rs 9.5555
1
<?php
2
3
/**
4
 * @author Marwan Al-Soltany <[email protected]>
5
 * @copyright Marwan Al-Soltany 2021
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace MAKS\Velox\Helper;
13
14
/**
15
 * A class that serves as a holder for various miscellaneous utility function.
16
 */
17
final class Misc
18
{
19
    /**
20
     * Gets a value from an array via dot-notation.
21
     *
22
     * @param array &$array The array to get the value from.
23
     * @param string $key The dotted key representation.
24
     * @param mixed $default [optional] The default fallback value.
25
     *
26
     * @return mixed The requested value if found otherwise the default parameter.
27
     */
28 58
    public static function getArrayValueByKey(array &$array, string $key, $default = null)
29
    {
30 58
        if (!count($array)) {
31 9
            return $default;
32
        }
33
34 58
        $data = &$array;
35
36 58
        if (strpos($key, '.') !== false) {
37 50
            $parts = explode('.', $key);
38
39 50
            foreach ($parts as $part) {
40 50
                if (!isset($data[$part])) {
41 32
                    return $default;
42
                }
43
44 50
                $data = &$data[$part];
45
            }
46
47 50
            return $data ?? $default;
48
        }
49
50 36
        return $data[$key] ?? $default;
51
    }
52
53
    /**
54
     * Sets a value of an array via dot-notation.
55
     *
56
     * @param array $array The array to set the value in.
57
     * @param string $key The dotted key representation.
58
     * @param mixed $value The value to set.
59
     *
60
     * @return bool True on success.
61
     */
62 77
    public static function setArrayValueByKey(array &$array, string $key, $value): bool
63
    {
64 77
        if (!strlen($key)) {
65 1
            return false;
66
        }
67
68 77
        $parts = explode('.', $key);
69 77
        $lastPart = array_pop($parts);
70
71 77
        $data = &$array;
72
73 77
        if (!empty($parts)) {
74 46
            foreach ($parts as $part) {
75 46
                if (!isset($data[$part])) {
76 6
                    $data[$part] = [];
77
                }
78
79 46
                $data = &$data[$part];
80
            }
81
        }
82
83 77
        $data[$lastPart] = $value;
84
85 77
        return true;
86
    }
87
88
    /**
89
     * Cuts a value of an array via dot-notation, the value will be returned and the key will be unset from the array.
90
     *
91
     * @param array $array The array to cut the value from.
92
     * @param string $key The dotted key representation.
93
     *
94
     * @return mixed The requested value if found otherwise the default parameter.
95
     */
96 11
    public static function cutArrayValueByKey(array &$array, string $key, $default = null)
97
    {
98 11
        $cut = function (&$array, $key) use ($default, &$cut) {
99 11
            if (!is_array($array)) {
100 1
                return;
101
            }
102
103 11
            if (count($parts = explode('.', $key)) > 1) {
104 3
                return $cut($array[$parts[0]], implode('.', array_slice($parts, 1)));
105
            }
106
107 11
            $value = $array[$key] ?? $default;
108
109 11
            unset($array[$key]);
110
111 11
            return $value;
112 11
        };
113
114 11
        return $cut($array, $key, $default);
115
    }
116
117
118
    /**
119
     * Gets a private, protected, or public property (default, static, or constant) of an object.
120
     *
121
     * @param object $object Class instance.
122
     * @param string $property Property name.
123
     *
124
     * @return mixed The property value.
125
     *
126
     * @throws \Exception On failure.
127
     */
128 10
    public static function getObjectProperty($object, string $property)
129
    {
130 10
        return \Closure::bind(function ($object, $property) {
131 10
            $return = null;
132
133
            try {
134 10
                $class = get_class($object);
135 10
                if (defined($class . '::' . $property)) {
136 1
                    $return = constant($class . '::' . $property);
137 10
                } elseif (isset($object::$$property)) {
138 9
                    $return = $object::$$property;
139 3
                } elseif (isset($object->{$property})) {
140 3
                    $return = $object->{$property};
141
                } else {
142 10
                    throw new \Exception("No default, static, or constant property with the name '{$property}' exists!");
143
                }
144 1
            } catch (\Exception $error) {
145 1
                throw new \Exception(sprintf('%s() failed!', __METHOD__), $error->getCode(), $error);
146
            }
147
148 10
            return $return;
149 10
        }, null, $object)($object, $property);
150
    }
151
152
    /**
153
     * Sets a private, protected, or public property (default or static) of an object.
154
     *
155
     * @param object $object Class instance.
156
     * @param string $property Property name.
157
     * @param mixed $value Property value.
158
     *
159
     * @return mixed The new property value.
160
     *
161
     * @throws \Exception On failure.
162
     */
163 46
    public static function setObjectProperty($object, string $property, $value)
164
    {
165 46
        return \Closure::bind(function ($object, $property, $value) {
166 46
            $return = null;
167
168
            try {
169 46
                if (isset($object::$$property)) {
170 44
                    $return = $object::$$property = $value;
171 3
                } elseif (isset($object->{$property})) {
172 3
                    $return = $object->{$property} = $value;
173
                } else {
174 46
                    throw new \Exception("No default, static, or constant property with the name '{$property}' exists!");
175
                }
176 1
            } catch (\Exception $error) {
177 1
                throw new \Exception(sprintf('%s() failed!', __METHOD__), $error->getCode(), $error);
178
            }
179
180 46
            return $return;
181 46
        }, null, $object)($object, $property, $value);
182
    }
183
184
    /**
185
     * Calls a private, protected, or public method on an object.
186
     *
187
     * @param object $object Class instance.
188
     * @param string $method Method name.
189
     * @param mixed ...$arguments
190
     *
191
     * @return mixed The function result, or false on error.
192
     *
193
     * @throws \Exception On failure or if the called function threw an exception.
194
     */
195 1
    public static function callObjectMethod($object, string $method, ...$arguments)
196
    {
197 1
        return \Closure::bind(function ($object, $method, $arguments) {
198
            try {
199 1
                return call_user_func_array([$object, $method], $arguments);
200 1
            } catch (\Exception $error) {
201 1
                throw new \Exception(sprintf('%s() failed!', __METHOD__), $error->getCode(), $error);
202
            }
203 1
        }, null, $object)($object, $method, $arguments);
204
    }
205
206
    /**
207
     * Interpolates context values into text placeholders.
208
     *
209
     * @param string $text The text to interpolate.
210
     * @param array $context An associative array like `['varName' => 'varValue']`.
211
     * @param string $placeholderIndicator The wrapper that indicate a variable. Max 2 chars, anything else will be ignored and "{}" will be used instead.
212
     *
213
     * @return string The interpolated string.
214
     */
215 20
    public static function interpolate(string $text, array $context = [], string $placeholderIndicator = '{}'): string
216
    {
217 20
        if (strlen($placeholderIndicator) !== 2) {
218 1
            $placeholderIndicator = '{}';
219
        }
220
221 20
        $replacements = [];
222 20
        foreach ($context as $key => $value) {
223
            // check that the value can be cast to string
224 20
            if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) {
225 20
                $replacements[$placeholderIndicator[0] . $key . $placeholderIndicator[1]] = $value;
226
            }
227
        }
228
229 20
        return strtr($text, $replacements);
230
    }
231
232
    /**
233
     * Transforms the case/content of a string by applying a one or more of the 20 available transformations.
234
     * The transformations are applied in the order they are specified.
235
     * Available transformations:
236
     * - `clean`: discards all meta-characters (@#$%&^*+=-~:;,.!?(){}[]|/\\'"\`), separates concatenated words [`ExampleString-num.1`, `Example String num 1`].
237
     * - `alnum`: removes every thing other that english letters, numbers and spaces. [`Example@123` -> `Example123`]
238
     * - `alpha`: removes every thing other that english letters. [`Example123` -> `Example`]
239
     * - `numeric`: removes every thing other that numbers. [`Example123` -> `123`]
240
     * - `slug`: lowercase, all letters to their A-Z representation (transliteration), spaces to dashes, no special characters (URL-safe) [`Example (String)` -> `example-string`].
241
     * - `title`: titlecase [`example string` -> `Example String`].
242
     * - `pascal`: titlecase, no spaces [`example string` -> `ExampleString`].
243
     * - `camel`: titlecase, no spaces, first letter lowercase [`example string` -> `exampleString`].
244
     * - `constant`: uppercase, spaces to underscores [`Example String` -> `EXAMPLE_STRING`].
245
     * - `snake`: lowercase, spaces to underscores [`Example String` -> `example_string`].
246
     * - `kebab`: lowercase, spaces to dashes [`Example String` -> `example-string`].
247
     * - `dot`: lowercase, spaces to dots [`Example String` -> `example.string`].
248
     * - `spaceless`: removes spaces [`Example String` -> `ExampleString`].
249
     * - A built-in function name from this list can also be used: `strtolower`, `strtoupper`, `lcfirst`, `ucfirst`, `ucwords`, `trim`, `ltrim`, `rtrim`.
250
     *
251
     * NOTE: Unknown transformations will be ignored silently.
252
     *
253
     * NOTE: The subject (string) loses some of its characteristics when a transformation is applied,
254
     * that means reversing the transformations will not guarantee getting the old subject back.
255
     *
256
     * @param string $subject The string to transform.
257
     * @param string ...$transformations One or more transformations to apply.
258
     *
259
     * @return string The transformed string.
260
     */
261 36
    public static function transform(string $subject, string ...$transformations): string
262
    {
263 36
        $specialMetaChars = '/[@\#\$%&\^\*\+\=\-~\:;,\.\!\?\(\)\{\}\[\]\|\/\\\\\'"`]+/';
264 36
        $transliterations = 'Any-Latin;Latin-ASCII;NFD;NFC;Lower();[:NonSpacing Mark:] Remove;[:Punctuation:] Remove;[:Other:] Remove;[\u0080-\u7fff] Remove;';
265
266 36
        static $cases = null;
267
268 36
        if ($cases === null) {
269
            $cases = [
270 36
                'clean'     => fn ($string) => static::transform(preg_replace([$specialMetaChars, '/(?<!^)[A-Z]/', '/[ ]+/'], [' ', ' $0', ' '], $string), 'trim'),
271 1
                'alnum'     => fn ($string) => static::transform(preg_replace('/[^a-zA-Z0-9 ]+/', '', $string), 'trim'),
272 1
                'alpha'     => fn ($string) => static::transform(preg_replace('/[^a-zA-Z]+/', '', $string), 'trim'),
273 1
                'numeric'   => fn ($string) => static::transform(preg_replace('/[^0-9]+/', '', $string), 'trim'),
274 2
                'slug'      => fn ($string) => static::transform(transliterator_transliterate($transliterations, preg_replace('/-+/', ' ', $string)), 'kebab'),
275 5
                'title'     => fn ($string) => static::transform($string, 'clean', 'ucwords'),
276 5
                'pascal'    => fn ($string) => static::transform($string, 'title', 'spaceless'),
277 5
                'camel'     => fn ($string) => static::transform($string, 'pascal', 'lcfirst'),
278 1
                'constant'  => fn ($string) => static::transform($string, 'snake', 'strtoupper'),
279 34
                'snake'     => fn ($string) => strtr(static::transform($string, 'clean', 'strtolower'), [' ' => '_']),
280 3
                'kebab'     => fn ($string) => strtr(static::transform($string, 'clean', 'strtolower'), [' ' => '-']),
281 1
                'dot'       => fn ($string) => strtr(static::transform($string, 'clean', 'strtolower'), [' ' => '.']),
282 5
                'spaceless' => fn ($string) => strtr($string, [' ' => '']),
283
                '*'         => ['strtolower', 'strtoupper', 'lcfirst', 'ucfirst', 'ucwords', 'trim', 'ltrim', 'rtrim'],
284
            ];
285
        }
286
287 36
        $functions = array_flip((array)$cases['*']);
288
289 36
        foreach ($transformations as $name) {
290 36
            $name = strtolower($name);
291
292 36
            if (isset($cases[$name]) && $name !== '*') {
293 36
                $subject = $cases[$name]($subject);
294
            } else {
295 36
                $index = $functions[$name] ?? -1;
296 36
                if ($index >= 0 && function_exists($cases['*'][$index])) {
297 36
                    $subject = $cases['*'][$index]($subject);
298
                }
299
            }
300
        }
301
302 36
        return $subject;
303
    }
304
305
    /**
306
     * Returns the passed key(s) from the backtrace.
307
     *
308
     * @param null|string|string[] $pluck [optional] $pluck The key to to get as a string or an array of strings (keys) from this list `[file, line, function, class, type, args]`, passing `null` will return the entire backtrace.
309
     * @param int $offset [optional] The offset of the backtrace, passing `-1` will reference the last item in the backtrace.
310
     *
311
     * @return string|int|array|null A string or int if a string is passed, an array if an array or null is passed, and null if no match was found.
312
     */
313 5
    public static function backtrace($pluck = null, ?int $offset = 0)
314
    {
315 5
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
316 5
        $plucked = null;
317
318 5
        if ($pluck === null) {
319 1
            return $backtrace;
320
        }
321
322 5
        if ($offset === -1) {
323 1
            $offset = 0;
324 1
            $backtrace = array_reverse($backtrace);
325
        }
326
327 5
        if (count($backtrace) < $offset + 1) {
328 1
            return null;
329 5
        } elseif (is_string($pluck)) {
330 3
            $plucked = isset($backtrace[$offset][$pluck]) ? $backtrace[$offset][$pluck] : null;
331 3
        } elseif (is_array($pluck)) {
0 ignored issues
show
introduced by
The condition is_array($pluck) is always true.
Loading history...
332 3
            $plucked = [];
333 3
            foreach ($pluck as $key) {
334 3
                !isset($backtrace[$offset][$key]) ?: $plucked[$key] = $backtrace[$offset][$key];
335
            }
336
        }
337
338 5
        return is_string($plucked) || is_array($plucked) && count($plucked, COUNT_RECURSIVE) ? $plucked : null;
339
    }
340
}
341