Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.43% |
54 / 56 |
|
83.33% |
10 / 12 |
CRAP | |
0.00% |
0 / 1 |
DeveloperToolbar | |
96.43% |
54 / 56 |
|
83.33% |
10 / 12 |
24 | |
0.00% |
0 / 1 |
__construct | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
3.00 | |||
getInstance | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRequestHash | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDataByHash | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
generateRequestHash | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
addData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setDisabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDisabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
dump | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getDumps | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__destruct | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
5.01 |
1 | <?php |
2 | |
3 | namespace Miniframe\Toolbar\Service; |
4 | |
5 | use Miniframe\Core\Config; |
6 | use Miniframe\Core\Request; |
7 | use RuntimeException; |
8 | |
9 | class DeveloperToolbar |
10 | { |
11 | /** |
12 | * Path for the log files. |
13 | * |
14 | * @var string |
15 | */ |
16 | private $logPath; |
17 | /** |
18 | * Path to the log file for the current request. |
19 | * |
20 | * @var string |
21 | */ |
22 | private $logFile; |
23 | /** |
24 | * Hash for the current request. |
25 | * |
26 | * @var string |
27 | */ |
28 | private $requestHash; |
29 | /** |
30 | * Data that is logged for the current request. |
31 | * |
32 | * @var array |
33 | */ |
34 | private $logData; |
35 | /** |
36 | * Set to true to disable logging for this request |
37 | * |
38 | * @var bool |
39 | */ |
40 | private $disabled = false; |
41 | |
42 | /** |
43 | * Log retention in seconds (14400s = 4h) |
44 | */ |
45 | private const LOG_RETENTION = 14400; |
46 | |
47 | /** |
48 | * Reference to a DeveloperToolbar instance |
49 | * |
50 | * @var DeveloperToolbar|null |
51 | */ |
52 | private static $developerToolbar; |
53 | |
54 | /** |
55 | * Initiates the Developer Toolbar service |
56 | * |
57 | * @param Request $request Reference to the Request object. |
58 | * @param string $logPath Path in which log files are stored. |
59 | */ |
60 | public function __construct(Request $request, string $logPath) |
61 | { |
62 | // Store basic data |
63 | $this->logData['rusage']['start'] = getrusage(); |
64 | $this->logData['request'] = $request; |
65 | $this->logData['requestHeaders'] = function_exists('getallheaders') ? getallheaders() : array(); |
66 | $this->logData['dumps'] = array(); |
67 | |
68 | // Defines the log file |
69 | $this->requestHash = $this->generateRequestHash(); |
70 | $this->logPath = $logPath; |
71 | if (!is_dir($this->logPath)) { |
72 | mkdir($this->logPath, 0777, true); |
73 | } |
74 | $this->logFile = rtrim($logPath, '/') . '/' . $this->requestHash . '.debug'; |
75 | |
76 | // Catches all kind of errors |
77 | $this->logData['errors'] = array(); |
78 | set_error_handler(function (int $errno, string $message, string $file, int $line): bool { |
79 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
80 | array_shift($backtrace); // Remove the error handler itself |
81 | $table = [ |
82 | E_ERROR => 'E_ERROR', |
83 | E_WARNING => 'E_WARNING', |
84 | E_PARSE => 'E_PARSE', |
85 | E_NOTICE => 'E_NOTICE', |
86 | E_CORE_ERROR => 'E_CORE_ERROR', |
87 | E_CORE_WARNING => 'E_CORE_WARNING', |
88 | E_COMPILE_ERROR => 'E_COMPILE_ERROR', |
89 | E_COMPILE_WARNING => 'E_COMPILE_WARNING', |
90 | E_USER_ERROR => 'E_USER_ERROR', |
91 | E_USER_WARNING => 'E_USER_WARNING', |
92 | E_USER_NOTICE => 'E_USER_NOTICE', |
93 | E_STRICT => 'E_STRICT', |
94 | E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', |
95 | E_DEPRECATED => 'E_DEPRECATED', |
96 | E_USER_DEPRECATED => 'E_USER_DEPRECATED', |
97 | ]; |
98 | $this->logData['errors'][] = array( |
99 | 'errno' => $errno, |
100 | 'errtype' => $table[$errno] ?? 'Unknown', |
101 | 'message' => $message, |
102 | 'file' => $file, |
103 | 'line' => $line, |
104 | 'backtrace' => $backtrace, |
105 | ); |
106 | return true; // Error catched/logged, don't show it in the page itself. |
107 | }, E_ALL); |
108 | |
109 | // Register for static calls (mainly dump()) |
110 | self::$developerToolbar = $this; |
111 | } |
112 | |
113 | /** |
114 | * Returns a reference to a DeveloperToolbar instance |
115 | * |
116 | * @return DeveloperToolbar|null |
117 | */ |
118 | public static function getInstance(): ?DeveloperToolbar |
119 | { |
120 | return self::$developerToolbar; |
121 | } |
122 | |
123 | /** |
124 | * Returns the request hash |
125 | * |
126 | * @return string |
127 | */ |
128 | public function getRequestHash(): string |
129 | { |
130 | return $this->requestHash; |
131 | } |
132 | |
133 | /** |
134 | * Fetched logged data by hash. |
135 | * |
136 | * @param string $hash The hash. |
137 | * |
138 | * @return array|null |
139 | */ |
140 | public function getDataByHash(string $hash): ?array |
141 | { |
142 | if (!preg_match('/^[a-zA-Z0-9_\-.]+$/', $hash)) { |
143 | return null; |
144 | } |
145 | $logFile = $this->logPath . '/' . $hash . '.debug'; |
146 | if (!file_exists($logFile)) { |
147 | return null; |
148 | } |
149 | return unserialize(file_get_contents($logFile)); |
150 | } |
151 | |
152 | /** |
153 | * Generates a somewhat random/unique string that matches /^[a-zA-Z0-9_\-.]+$/ |
154 | * |
155 | * @return string |
156 | */ |
157 | private function generateRequestHash(): string |
158 | { |
159 | $binary = ''; |
160 | foreach (str_split(date('YmdHis') . substr(explode(' ', microtime())[0], 2), 2) as $char) { |
161 | $binary .= chr($char); |
162 | } |
163 | for ($iterator = 0; $iterator < 4; ++$iterator) { |
164 | $binary .= chr(rand(0x00, 0xff)); |
165 | } |
166 | return str_replace(['+', '/', '='], ['_', '-', '.'], base64_encode($binary)); |
167 | } |
168 | |
169 | /** |
170 | * Adds data to the Developer Toolbar log |
171 | * |
172 | * @param string $key Key of the data. |
173 | * @param mixed $data Serializable log data. |
174 | * |
175 | * @return void |
176 | */ |
177 | public function addData(string $key, $data): void |
178 | { |
179 | $this->logData[$key] = $data; |
180 | } |
181 | |
182 | /** |
183 | * Disables or enables logging. |
184 | * |
185 | * @param boolean $isDisabled The disabled state. |
186 | * |
187 | * @return void |
188 | */ |
189 | public function setDisabled(bool $isDisabled): void |
190 | { |
191 | $this->disabled = $isDisabled; |
192 | } |
193 | |
194 | /** |
195 | * Return the disabled state. |
196 | * |
197 | * @return boolean |
198 | */ |
199 | public function isDisabled(): bool |
200 | { |
201 | return $this->disabled; |
202 | } |
203 | |
204 | /** |
205 | * Dumps a variable |
206 | * |
207 | * @param mixed $variable The variable to dump. |
208 | * |
209 | * @return void |
210 | */ |
211 | public function dump($variable): void |
212 | { |
213 | // Where is the dump() called? |
214 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); |
215 | $backtraceIndex = 0; |
216 | if ( |
217 | isset($backtrace[$backtraceIndex]['file']) |
218 | && realpath($backtrace[$backtraceIndex]['file']) == realpath(__DIR__ . '/../functions.php') |
219 | ) { |
220 | $backtraceIndex = 1; |
221 | } |
222 | $data = [ |
223 | 'file' => $backtrace[$backtraceIndex]['file'] ?? null, |
224 | 'line' => $backtrace[$backtraceIndex]['line'] ?? null, |
225 | 'data' => $variable, |
226 | ]; |
227 | |
228 | $this->logData['dumps'][] = $data; |
229 | } |
230 | |
231 | /** |
232 | * Returns all dumps |
233 | * |
234 | * @return array |
235 | */ |
236 | public function getDumps(): array |
237 | { |
238 | return $this->logData['dumps'] ?? []; |
239 | } |
240 | |
241 | /** |
242 | * Returns all errors |
243 | * |
244 | * @return array |
245 | */ |
246 | public function getErrors(): array |
247 | { |
248 | return $this->logData['errors'] ?? []; |
249 | } |
250 | |
251 | /** |
252 | * Store the log data and cleans up old logs |
253 | */ |
254 | public function __destruct() |
255 | { |
256 | if ($this->disabled) { |
257 | return; |
258 | } |
259 | |
260 | $this->logData['rusage']['end'] = getrusage(); |
261 | $this->logData['memoryPeak']['real'] = memory_get_peak_usage(true); |
262 | $this->logData['memoryPeak']['emalloc'] = memory_get_peak_usage(); |
263 | file_put_contents($this->logFile, serialize($this->logData)); |
264 | if (!file_exists($this->logFile)) { |
265 | throw new \RuntimeException('Can\'t write to ' . $this->logFile); |
266 | } |
267 | |
268 | // Clean up old files |
269 | $files = glob($this->logPath . '/*.debug'); |
270 | foreach ($files as $file) { |
271 | if (filemtime($file) < time() - self::LOG_RETENTION) { |
272 | unlink($file); |
273 | } |
274 | } |
275 | } |
276 | } |