Misc::callObjectMethod()   A
last analyzed

Complexity

Conditions 2
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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