Passed
Push — master ( fab1ff...bbc3bd )
by Marwan
09:15
created

Misc::getArrayValueByKey()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 11
c 1
b 0
f 0
nc 6
nop 3
dl 0
loc 23
ccs 12
cts 12
cp 1
crap 6
rs 9.2222
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
use MAKS\Velox\Backend\Config;
15
16
/**
17
 * A class that serves as a holder for various miscellaneous utility function.
18
 */
19
final class Misc
20
{
21
    /**
22
     * Returns a normalized path based on OS.
23
     *
24
     * @param string $directory
25
     * @param string $filename
26
     * @param string $extension
27
     *
28
     * @return string
29
     */
30 9
    public static function getNormalizedPath(string $directory, string $filename, string $extension = ''): string
31
    {
32 9
        $filename = substr($filename, -strlen($extension)) === $extension ? $filename : $filename . $extension;
33 9
        $directory = $directory . '/';
34
35 9
        return preg_replace('/\/+|\\+/', DIRECTORY_SEPARATOR, $directory . $filename);
36
    }
37
38
    /**
39
     * Gets a value from an array via dot-notation.
40
     *
41
     * @param array &$array The array to get the value from.
42
     * @param string $key The dotted key representation.
43
     * @param mixed $default [optional] The default fallback value.
44
     *
45
     * @return mixed The requested value if found otherwise the default parameter.
46
     */
47 35
    public static function getArrayValueByKey(array &$array, string $key, $default = null)
48
    {
49 35
        if (!count($array)) {
50 3
            return $default;
51
        }
52
53 35
        $data = &$array;
54
55 35
        if (strpos($key, '.') !== false) {
56 21
            $parts = explode('.', $key);
57
58 21
            foreach ($parts as $part) {
59 21
                if (!array_key_exists($part, $data)) {
60 4
                    return $default;
61
                }
62
63 21
                $data = &$data[$part];
64
            }
65
66 21
            return $data;
67
        }
68
69 19
        return array_key_exists($key, $data) ? $data[$key] : $default;
70
    }
71
72
    /**
73
     * Sets a value of an array via dot-notation.
74
     *
75
     * @param array $array The array to set the value in.
76
     * @param string $key The string key representation.
77
     * @param mixed $value The value to set.
78
     *
79
     * @return bool True on success.
80
     */
81 31
    public static function setArrayValueByKey(array &$array, string $key, $value): bool
82
    {
83 31
        if (!strlen($key)) {
84 1
            return false;
85
        }
86
87 31
        $parts = explode('.', $key);
88 31
        $lastPart = array_pop($parts);
89
90 31
        $data = &$array;
91
92 31
        if (!empty($parts)) {
93 6
            foreach ($parts as $part) {
94 6
                if (!isset($data[$part])) {
95 4
                    $data[$part] = [];
96
                }
97
98 6
                $data = &$data[$part];
99
            }
100
        }
101
102 31
        $data[$lastPart] = $value;
103
104 31
        return true;
105
    }
106
107
108
    /**
109
     * Gets a private, protected, or public property (default, static, or constant) of an object.
110
     *
111
     * @param object $object Class instance.
112
     * @param string $property Property name.
113
     *
114
     * @return mixed The property value.
115
     *
116
     * @throws \Exception On failure.
117
     */
118 6
    public static function getObjectProperty($object, string $property)
119
    {
120 6
        return \Closure::bind(function ($object, $property) {
121 6
            $return = null;
122
123
            try {
124 6
                $class = get_class($object);
125 6
                if (defined($class . '::' . $property)) {
126 1
                    $return = constant($class . '::' . $property);
127 6
                } elseif (isset($object::$$property)) {
128 5
                    $return = $object::$$property;
129 3
                } elseif (isset($object->{$property})) {
130 3
                    $return = $object->{$property};
131
                } else {
132 6
                    throw new \Exception("No default, static, or constant property with the name '{$property}' exists!");
133
                }
134 1
            } catch (\Exception $error) {
135 1
                throw new \Exception(sprintf('%s() failed!', __METHOD__), $error->getCode(), $error);
136
            }
137
138 6
            return $return;
139 6
        }, null, $object)($object, $property);
140
    }
141
142
    /**
143
     * Sets a private, protected, or public property (default or static) of an object.
144
     *
145
     * @param object $object Class instance.
146
     * @param string $property Property name.
147
     * @param string $value Property value.
148
     *
149
     * @return mixed The new property value.
150
     *
151
     * @throws \Exception On failure.
152
     */
153 12
    public static function setObjectProperty($object, string $property, $value)
154
    {
155 12
        return \Closure::bind(function ($object, $property, $value) {
156 12
            $return = null;
157
158
            try {
159 12
                if (isset($object::$$property)) {
160 12
                    $return = $object::$$property = $value;
161 1
                } elseif (isset($object->{$property})) {
162 1
                    $return = $object->{$property} = $value;
163
                } else {
164 12
                    throw new \Exception("No default, static, or constant property with the name '{$property}' exists!");
165
                }
166 1
            } catch (\Exception $error) {
167 1
                throw new \Exception(sprintf('%s() failed!', __METHOD__), $error->getCode(), $error);
168
            }
169
170 12
            return $return;
171 12
        }, null, $object)($object, $property, $value);
172
    }
173
174
    /**
175
     * Calls a private, protected, or public method on an object.
176
     *
177
     * @param object $object Class instance.
178
     * @param string $method Method name.
179
     * @param mixed ...$arguments
180
     *
181
     * @return mixed The function result, or false on error.
182
     *
183
     * @throws \Exception On failure or if the called function threw an exception.
184
     */
185 1
    public static function callObjectMethod($object, string $method, ...$arguments)
186
    {
187 1
        return \Closure::bind(function ($object, $method, $arguments) {
188
            try {
189 1
                return call_user_func_array([$object, $method], $arguments);
190 1
            } catch (\Exception $error) {
191 1
                throw new \Exception(sprintf('%s() failed!', __METHOD__), $error->getCode(), $error);
192
            }
193 1
        }, null, $object)($object, $method, $arguments);
194
    }
195
196
    /**
197
     * Interpolates context values into text placeholders.
198
     *
199
     * @param string $text The text to interpolate.
200
     * @param array $context An associative array like `['varName' => 'varValue']`.
201
     * @param string $placeholderIndicator The wrapper that indicate a variable. Max 2 chars, anything else will be ignored and "{}" will be used instead.
202
     *
203
     * @return string The interpolated string.
204
     */
205 10
    public static function interpolate(string $text, array $context = [], string $placeholderIndicator = '{}'): string
206
    {
207 10
        if (strlen($placeholderIndicator) !== 2) {
208 1
            $placeholderIndicator = '{}';
209
        }
210
211 10
        $replacements = [];
212 10
        foreach ($context as $key => $value) {
213
            // check that the value can be cast to string
214 10
            if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) {
215 10
                $replacements[$placeholderIndicator[0] . $key . $placeholderIndicator[1]] = $value;
216
            }
217
        }
218
219 10
        return strtr($text, $replacements);
220
    }
221
222
    /**
223
     * Returns the passed key(s) from the backtrace.
224
     *
225
     * @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.
226
     * @param int $offset [optional] The offset of the backtrace, passing `-1` will reference the last item in the backtrace.
227
     *
228
     * @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.
229
     */
230 5
    public static function backtrace($pluck = null, ?int $offset = 0)
231
    {
232 5
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
233 5
        $plucked = null;
234
235 5
        if ($pluck === null) {
236 1
            return $backtrace;
237
        }
238
239 5
        if ($offset === -1) {
240 1
            $offset = 0;
241 1
            $backtrace = array_reverse($backtrace);
242
        }
243
244 5
        if (count($backtrace) < $offset + 1) {
245 1
            return null;
246 5
        } elseif (is_string($pluck)) {
247 3
            $plucked = isset($backtrace[$offset][$pluck]) ? $backtrace[$offset][$pluck] : null;
248 3
        } elseif (is_array($pluck)) {
0 ignored issues
show
introduced by
The condition is_array($pluck) is always true.
Loading history...
249 3
            $plucked = [];
250 3
            foreach ($pluck as $key) {
251 3
                !isset($backtrace[$offset][$key]) ?: $plucked[$key] = $backtrace[$offset][$key];
252
            }
253
        }
254
255 5
        return is_string($plucked) || is_array($plucked) && count($plucked, COUNT_RECURSIVE) ? $plucked : null;
256
    }
257
258
    /**
259
     * Logs a message to a file and generates it if it does not exist.
260
     *
261
     * @param string $message The message wished to be logged.
262
     * @param array|null $context An associative array of values where key = {key} in the message (context).
263
     * @param string|null $filename [optional] The name wished to be given to the file. If not provided the message will be logged in "autogenerated-{Ymd}.log".
264
     * @param string|null $directory [optional] The directory where the log file should be written. If not the file will be written in "/storage/logs/".
265
     *
266
     * @return bool True if message was written.
267
     */
268 4
    public static function log(string $message, ?array $context = [], ?string $filename = null, ?string $directory = null): bool
269
    {
270 4
        if (!Config::get('global.loggingEnabled')) {
271 1
            return true;
272
        }
273
274 4
        $hasPassed = false;
275
276 4
        if (!$filename) {
277 1
            $filename = 'autogenerated-' . date('Ymd');
278
        }
279
280 4
        if (!$directory) {
281 4
            $directory = BASE_PATH . '/storage/logs/';
282
        }
283
284 4
        $file = self::getNormalizedPath($directory, $filename, '.log');
285
286 4
        if (!file_exists($directory)) {
287 2
            mkdir($directory, 0744, true);
288
        }
289
290
        // create log file if it does not exist
291 4
        if (!is_file($file) && is_writable($directory)) {
292 2
            $signature = 'Created by ' . __METHOD__ . date('() \o\\n l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL . PHP_EOL;
293 2
            file_put_contents($file, $signature, 0, stream_context_create());
294 2
            chmod($file, 0775);
295
        }
296
297
        // write in the log file
298 4
        if (is_writable($file)) {
299 4
            clearstatcache(true, $file);
300
            // empty the file if it exceeds 64MB
301 4
            if (filesize($file) > 6.4e+7) {
302
                $stream = fopen($file, 'r');
303
                if (is_resource($stream)) {
304
                    $signature = fgets($stream) . 'For exceeding 64MB, it was overwritten on ' . date('l jS \of F Y h:i:s A (Ymdhis)') . PHP_EOL . PHP_EOL;
305
                    fclose($stream);
306
                    file_put_contents($file, $signature, 0, stream_context_create());
307
                    chmod($file, 0775);
308
                }
309
            }
310
311 4
            $timestamp = (new \DateTime())->format(DATE_ISO8601);
312 4
            $message   = self::interpolate($message, $context ?? []);
313
314 4
            $log = "$timestamp\t$message\n";
315
316 4
            $stream = fopen($file, 'a+');
317 4
            if (is_resource($stream)) {
318 4
                fwrite($stream, $log);
319 4
                fclose($stream);
320 4
                $hasPassed = true;
321
            }
322
        }
323
324 4
        return $hasPassed;
325
    }
326
}
327