Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.48% |
129 / 138 |
|
68.42% |
13 / 19 |
CRAP | |
0.00% |
0 / 1 |
GeneralUtils | |
93.48% |
129 / 138 |
|
68.42% |
13 / 19 |
56.87 | |
0.00% |
0 / 1 |
checkNotNull | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
checkNotFalse | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
checkNotBool | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
checkNotEmpty | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getCheckErrorMessage | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
escape | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
unescape | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
base64EncodeUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
base64DecodeUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
encrypt | |
88.24% |
15 / 17 |
|
0.00% |
0 / 1 |
5.04 | |||
getRandomIvForAlgo | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
decrypt | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
3.01 | |||
hash | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
binarySearch | |
85.00% |
17 / 20 |
|
0.00% |
0 / 1 |
8.22 | |||
getPrettyTrace | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getTraceOverview | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
7.01 | |||
measureLatency | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
removeRecursive | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
8.04 | |||
fromEnv | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace Olz\Utils; |
4 | |
5 | class GeneralUtils { |
6 | use WithUtilsTrait; |
7 | |
8 | // Error handling |
9 | |
10 | /** |
11 | * @phpstan-assert !null $value |
12 | * |
13 | * @param string|callable(): string $error_message |
14 | */ |
15 | public function checkNotNull(mixed $value, string|callable $error_message): void { |
16 | if ($value === null) { |
17 | $message = $this->getCheckErrorMessage($error_message); |
18 | $this->log()->error($message); |
19 | throw new \Exception($message); |
20 | } |
21 | } |
22 | |
23 | /** |
24 | * @phpstan-assert !false $value |
25 | * |
26 | * @param string|callable(): string $error_message |
27 | */ |
28 | public function checkNotFalse(mixed $value, string|callable $error_message): void { |
29 | if ($value === false) { |
30 | $message = $this->getCheckErrorMessage($error_message); |
31 | $this->log()->error($message); |
32 | throw new \Exception($message); |
33 | } |
34 | } |
35 | |
36 | /** |
37 | * @phpstan-assert !bool $value |
38 | * |
39 | * @param string|callable(): string $error_message |
40 | */ |
41 | public function checkNotBool(mixed $value, string|callable $error_message): void { |
42 | if (is_bool($value)) { |
43 | $message = $this->getCheckErrorMessage($error_message); |
44 | $this->log()->error($message); |
45 | throw new \Exception($message); |
46 | } |
47 | } |
48 | |
49 | /** |
50 | * @phpstan-assert !'' $value |
51 | * |
52 | * @param string|callable(): string $error_message |
53 | */ |
54 | public function checkNotEmpty(mixed $value, string|callable $error_message): void { |
55 | if ($value === '') { |
56 | $message = $this->getCheckErrorMessage($error_message); |
57 | $this->log()->error($message); |
58 | throw new \Exception($message); |
59 | } |
60 | } |
61 | |
62 | /** @param string|callable(): string $error_message */ |
63 | protected function getCheckErrorMessage(string|callable $error_message): string { |
64 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); |
65 | $app_env = $this->envUtils()->getAppEnv(); |
66 | $is_test = $app_env === 'test'; |
67 | $file = basename($trace[1]['file'] ?? ''); |
68 | $line = $is_test ? "***" : $trace[1]['line'] ?? ''; |
69 | $callsite = "{$file}:{$line}"; |
70 | if (is_string($error_message)) { |
71 | return "{$callsite} {$error_message}"; |
72 | } |
73 | $computed_error_message = $error_message(); |
74 | return "{$callsite} {$computed_error_message}"; |
75 | } |
76 | |
77 | // Escape |
78 | |
79 | /** @param array<string> $tokens */ |
80 | public function escape(string $string, array $tokens): string { |
81 | $esc_tokens = array_map(fn ($token) => "\\{$token}", $tokens); |
82 | return str_replace($tokens, $esc_tokens, $string); |
83 | } |
84 | |
85 | /** @param array<string> $tokens */ |
86 | public function unescape(string $string, array $tokens): string { |
87 | $esc_tokens = array_map(fn ($token) => "\\{$token}", $tokens); |
88 | return str_replace($esc_tokens, $tokens, $string); |
89 | } |
90 | |
91 | // Base64 |
92 | |
93 | public function base64EncodeUrl(string $string): string { |
94 | return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($string)); |
95 | } |
96 | |
97 | public function base64DecodeUrl(string $string): string { |
98 | return base64_decode(str_replace(['-', '_'], ['+', '/'], $string)); |
99 | } |
100 | |
101 | // Crypto |
102 | |
103 | public function encrypt(string $key, mixed $data): string { |
104 | $plaintext = json_encode($data); |
105 | if (!$plaintext) { |
106 | throw new \Exception("encrypt: json_encode failed"); |
107 | } |
108 | $algo = 'aes-256-gcm'; |
109 | $iv = $this->getRandomIvForAlgo($algo); |
110 | $ciphertext = openssl_encrypt($plaintext, $algo, $key, OPENSSL_RAW_DATA, $iv, $tag); |
111 | if (!$ciphertext) { |
112 | throw new \Exception("encrypt: openssl_encrypt failed"); |
113 | } |
114 | $result = json_encode([ |
115 | 'algo' => $algo, |
116 | 'iv' => $this->base64EncodeUrl($iv), |
117 | 'tag' => $tag ? $this->base64EncodeUrl($tag) : null, |
118 | 'ciphertext' => $this->base64EncodeUrl($ciphertext), |
119 | ]); |
120 | if (!$result) { |
121 | throw new \Exception("encrypt: this should never happen"); |
122 | } |
123 | return $this->base64EncodeUrl($result); |
124 | } |
125 | |
126 | protected function getRandomIvForAlgo(string $algo): string { |
127 | $length = openssl_cipher_iv_length($algo); |
128 | if ($length === false) { |
129 | throw new \Exception("Unknown openssl_cipher_iv_length({$algo})"); |
130 | } |
131 | if ($length === 0) { |
132 | return ''; |
133 | } |
134 | return openssl_random_pseudo_bytes($length); |
135 | } |
136 | |
137 | public function decrypt(string $key, string $token): mixed { |
138 | $decrypt_data = json_decode($this->base64DecodeUrl($token), true); |
139 | if (!$decrypt_data) { |
140 | throw new \Exception("decrypt: json_decode failed"); |
141 | } |
142 | $ciphertext = $this->base64DecodeUrl($decrypt_data['ciphertext']); |
143 | $algo = $decrypt_data['algo'] ?? 'aes-256-gcm'; |
144 | $iv = $this->base64DecodeUrl($decrypt_data['iv']); |
145 | $tag = $this->base64DecodeUrl($decrypt_data['tag']); |
146 | $plaintext = openssl_decrypt($ciphertext, $algo, $key, OPENSSL_RAW_DATA, $iv, $tag); |
147 | if (!$plaintext) { |
148 | throw new \Exception("decrypt: openssl_decrypt failed"); |
149 | } |
150 | return json_decode($plaintext, true); |
151 | } |
152 | |
153 | public function hash(string $key, string $data): string { |
154 | $result = hash_hmac('sha256', $data, $key, true); |
155 | return $this->base64EncodeUrl($result); |
156 | } |
157 | |
158 | // Algorithms |
159 | |
160 | /** @return array{0: int, 1: int<-1, 1>} */ |
161 | public function binarySearch(callable $compare_fn, int $start, int $end): array { |
162 | $search_start = $start; |
163 | $search_end = $end; |
164 | if ($search_start === $search_end) { |
165 | return [0, 0]; |
166 | } |
167 | while ($search_start < $search_end) { |
168 | $probe_index = (int) floor(($search_start + $search_end) / 2); |
169 | $result = $compare_fn($probe_index); |
170 | if ($result < 0) { |
171 | $search_end = $probe_index; |
172 | } elseif ($result > 0) { |
173 | $search_start = $probe_index + 1; |
174 | } else { |
175 | return [$probe_index, 0]; |
176 | } |
177 | } |
178 | if ($search_start === $end) { |
179 | return [$end - 1, 1]; |
180 | } |
181 | $result = $compare_fn($search_start); |
182 | if ($result < 0) { |
183 | return [$search_start, -1]; |
184 | } |
185 | if ($result > 0) { |
186 | return [$search_start, 1]; |
187 | } |
188 | return [$search_start, 0]; |
189 | } |
190 | |
191 | // Debugging |
192 | |
193 | /** @param array<array<string, mixed>> $trace */ |
194 | public function getPrettyTrace(array $trace): string { |
195 | $output = 'Stack trace:'.PHP_EOL; |
196 | |
197 | $trace_len = count($trace); |
198 | for ($i = 0; $i < $trace_len; $i++) { |
199 | $entry = $trace[$i]; |
200 | |
201 | $func = $entry['function'].'('; |
202 | $args_len = count($entry['args']); |
203 | for ($j = 0; $j < $args_len; $j++) { |
204 | $func .= json_encode($entry['args'][$j]); |
205 | if ($j < $args_len - 1) { |
206 | $func .= ', '; |
207 | } |
208 | } |
209 | $func .= ')'; |
210 | |
211 | $output .= "#{$i} {$entry['file']}:{$entry['line']} - {$func}\n"; |
212 | } |
213 | return $output; |
214 | } |
215 | |
216 | /** @param array<array<string, mixed>> $trace */ |
217 | public function getTraceOverview(array $trace, int $first_index = 1): string { |
218 | $output = ''; |
219 | $trace_len = count($trace); |
220 | $last_class_name = null; |
221 | for ($i = $trace_len - 1; $i >= $first_index; $i--) { |
222 | $entry = $trace[$i]; |
223 | |
224 | $class_name = $entry['class'] ?? ''; |
225 | if ( |
226 | $class_name === '' |
227 | || $class_name === $last_class_name |
228 | || substr($class_name, 0, 4) !== 'Olz\\' |
229 | ) { |
230 | continue; |
231 | } |
232 | $reflection_class = new \ReflectionClass($class_name); |
233 | if ($reflection_class->isAbstract()) { |
234 | continue; |
235 | } |
236 | $last_class_name = $class_name; |
237 | $base_class_name = substr($class_name, strrpos($class_name, '\\') + 1); |
238 | |
239 | if ($output !== '') { |
240 | $output .= ">"; |
241 | } |
242 | $output .= "{$base_class_name}"; |
243 | } |
244 | return $output; |
245 | } |
246 | |
247 | /** @return array<mixed> */ |
248 | public function measureLatency(callable $fn): array { |
249 | $before = microtime(true); |
250 | $result = $fn(); |
251 | $duration = round((microtime(true) - $before) * 1000, 1); |
252 | $msg = "took {$duration}ms"; |
253 | return [$result, $msg]; |
254 | } |
255 | |
256 | // Tools |
257 | |
258 | public function removeRecursive(string $path): void { |
259 | if (is_dir($path)) { |
260 | $entries = scandir($path) ?: []; |
261 | foreach ($entries as $entry) { |
262 | if ($entry === '.' || $entry === '..') { |
263 | continue; |
264 | } |
265 | $entry_path = realpath("{$path}/{$entry}"); |
266 | if (!$entry_path) { |
267 | continue; |
268 | } |
269 | $this->removeRecursive($entry_path); |
270 | } |
271 | rmdir($path); |
272 | } elseif (is_file($path)) { |
273 | unlink($path); |
274 | } |
275 | } |
276 | |
277 | public static function fromEnv(): self { |
278 | return new self(); |
279 | } |
280 | } |