Source Code: SmartFactory/DebugProfiler.php

  1: <?php
  2: /**
  3:  * This file contains the implementation of the interface IDebugProfiler
  4:  * in the class DebugProfiler for debugging, tracing and profiling.
  5:  *
  6:  * @package System
  7:  *
  8:  * @author Oleg Schildt
  9:  */
 10: 
 11: namespace SmartFactory;
 12: 
 13: use \SmartFactory\Interfaces\IDebugProfiler;
 14: 
 15: /**
 16:  * Class for debugging, tracing and profiling.
 17:  *
 18:  * @author Oleg Schildt
 19:  */
 20: class DebugProfiler implements IDebugProfiler
 21: {
 22:     /**
 23:      * Internal variable for storing the log path.
 24:      *
 25:      * @var ?string
 26:      *
 27:      * @author Oleg Schildt
 28:      */
 29:     protected ?string $log_path = null;
 30: 
 31:     /**
 32:      * Internal variable for storing the flag that defines whether for each debug output the source file and
 33:      * line number are written from where it is called.
 34:      *
 35:      * @var bool
 36:      *
 37:      * @author Oleg Schildt
 38:      */
 39:     protected bool $write_source_file_and_line_by_debug = false;
 40: 
 41:     /**
 42:      * Internal variable for storing the time by profiling.
 43:      *
 44:      * @var int
 45:      *
 46:      * @see DebugProfiler::startProfilePoint()
 47:      * @see DebugProfiler::fixProfilePoint()
 48:      *
 49:      * @author Oleg Schildt
 50:      */
 51:     private static int $profile_time;
 52: 
 53:     /**
 54:      * Initializes the debug profiler with parameters.
 55:      *
 56:      * @param array $parameters
 57:      * Settings for logging as an associative array in the form key => value:
 58:      *
 59:      * - $parameters["log_path"] - the target file path where the logs should be stored.
 60:      *
 61:      * - $parameters["write_source_file_and_line_by_debug"] - the flag that defines whether for each debug output the source file and
 62:      * line number are written from where it is called.
 63:      *
 64:      * @return void
 65:      *
 66:      * @throws \Exception
 67:      * It might throw an exception in the case of any system errors:
 68:      *
 69:      * - if the log path is not specified.
 70:      * - if the trace file is not writable.
 71:      *
 72:      * @author Oleg Schildt
 73:      */
 74:     public function init(array $parameters): void
 75:     {
 76:         $this->write_source_file_and_line_by_debug = !empty($parameters["write_source_file_and_line_by_debug"]);
 77: 
 78:         if (empty($parameters["log_path"])) {
 79:             throw new \Exception("Log path is not specified!");
 80:         }
 81: 
 82:         $this->log_path = rtrim(str_replace("\\", "/", $parameters["log_path"]), "/") . "/";
 83: 
 84:         if (!file_exists($this->log_path) || !is_writable($this->log_path)) {
 85:             throw new \Exception(sprintf("The log path '%s' is not writable!", $this->log_path));
 86:         }
 87:     }
 88: 
 89:     /**
 90:      * Sets the flag that defines whether the file name and the line number should be written along with the message.
 91:      *
 92:      * @param bool $state
 93:      * the flag that defines whether the file name and the line number should be written along with the message.
 94:      *
 95:      * @return void
 96:      *
 97:      * @author Oleg Schildt
 98:      */
 99:     public function enableFileAndLineDetails(bool $state): void
100:     {
101:         $this->write_source_file_and_line_by_debug = $state;
102:     }
103: 
104:     /**
105:      * Extracts the call stack from the standard PHP backtrace (debug_backtrace).
106:      *
107:      * @param array $btrace
108:      * The backtrace.
109:      *
110:      * @return string
111:      * Returns the extracted call stack.
112:      *
113:      * @author Oleg Schildt
114:      */
115:     protected function extract_call_stack(array $btrace): string
116:     {
117:         if (empty($btrace)) {
118:             return "";
119:         }
120: 
121:         $trace = "";
122: 
123:         $indent = "";
124:         foreach ($btrace as $btrace_entry) {
125: 
126:             if (!empty($btrace_entry["function"]) && ($btrace_entry["function"] == "handle_error" || str_contains($btrace_entry["function"], "{closure}") || $btrace_entry["function"] == "handleError" || $btrace_entry["function"] == "trigger_error")) {
127:                 continue;
128:             }
129: 
130:             if (empty($btrace_entry["file"])) {
131:                 continue;
132:             }
133: 
134:             if (!empty($btrace_entry["function"])) {
135:                 $function = $btrace_entry["function"];
136: 
137:                 if (preg_match("/{closure:([^}{]+)}/", $function, $matches)) {
138:                     $trace .= $indent . "{closure:" . $this->trim_path(str_replace("\\", "/", $matches[1])) . "}" . "(";
139:                 } else {
140:                     $trace .= $indent . str_replace("SmartFactory\\", "", $function) . "(";
141:                 }
142: 
143:                 $trace .= ")";
144:             }
145: 
146:             $trace .= " [";
147: 
148:             $trace .= $this->trim_path(str_replace("\\", "/", $btrace_entry["file"]));
149: 
150:             $trace .= ", ";
151: 
152:             if (empty($btrace_entry["line"])) {
153:                 $trace .= "line number undefined";
154:             } else {
155:                 $trace .= $btrace_entry["line"];
156:             }
157: 
158:             $trace .= "]";
159: 
160:             $trace .= "\r\n";
161: 
162:             $indent .= "  ";
163:         }
164: 
165:         return trim($trace);
166:     } // extract_call_stack
167: 
168:     /**
169:      * Logs a message to a standard debug output (logs/debug.log).
170:      *
171:      * @param string $message
172:      * Message to be logged.
173:      *
174:      * @param bool $write_call_stack
175:      * Flag that defines where the call stack should be written.
176:      *
177:      * @param string $file_name
178:      * The target file name.
179:      *
180:      * @return void
181:      *
182:      * @throws \Exception
183:      * It might throw an exception in the case of any errors:
184:      *
185:      * - if the debug file is not writable.
186:      *
187:      * @author Oleg Schildt
188:      */
189:     public function debugMessage(string $message, bool $write_call_stack = false, string $file_name = "debug.log"): void
190:     {
191:         $logfile = $this->log_path . $file_name;
192: 
193:         $backfiles = debug_backtrace();
194: 
195:         if (basename($backfiles[0]['file']) == "short_functions_inc.php" && $backfiles[0]['function'] == "debugMessage") {
196:             $file = empty($backfiles[1]['file']) ? "" : $backfiles[1]['file'];
197:             $line = empty($backfiles[1]['line']) ? "" : $backfiles[1]['line'];
198:         } else {
199:             $file = empty($backfiles[0]['file']) ? "" : $backfiles[0]['file'];
200:             $line = empty($backfiles[0]['line']) ? "" : $backfiles[0]['line'];
201:         }
202: 
203:         $prefix = "";
204: 
205:         if ($this->write_source_file_and_line_by_debug && !empty($file) && !empty($line)) {
206:             $prefix = "#URI: " . ($_SERVER["REQUEST_URI"] ?? "") . "\r\n" .
207:                 "#Source: " . $this->trim_path(str_replace("\\", "/", $file)) . ", " . $line;
208:         }
209: 
210:         if ($write_call_stack) {
211:             $prefix .= "\r\n#Callstack:\r\n" . $this->extract_call_stack($backfiles);
212:         }
213: 
214:         $prefix .= "\r\n\r\n";
215: 
216:         $message = $prefix . trim($message);
217: 
218:         $message .= "\r\n-------\r\n";
219: 
220:         if ((!file_exists($logfile) && is_writable($this->log_path)) || is_writable($logfile)) {
221:             if (file_put_contents($logfile, $message, FILE_APPEND) === false) {
222:                 throw new \Exception(sprintf("Error by writing the log file '%s'!", $logfile));
223:             }
224:         } else {
225:             throw new \Exception(sprintf("The log file '%s' is not writable!", $logfile));
226:         }
227:     } // debugMessage
228: 
229:     /**
230:      * Logs a message to a standard profiling output (logs/profile.log)
231:      * and stores the current time.
232:      *
233:      * @param string $message
234:      * Message to be logged.
235:      *
236:      * @return void
237:      *
238:      * @throws \Exception
239:      * It might throw an exception in the case of any errors:
240:      *
241:      * - if the profile file is not writable.
242:      *
243:      * @see DebugProfiler::fixProfilePoint()
244:      *
245:      * @author Oleg Schildt
246:      */
247:     public function startProfilePoint(string $message): void
248:     {
249:         $this->debugMessage($message, false,"profile.log");
250: 
251:         self::$profile_time = microtime(true);
252:     } // startProfilePoint
253: 
254:     /**
255:      * Logs a message to a standard profiling output (logs/profile.log) and shows
256:      * the time elapsed since the last call startProfilePoint or
257:      * fixProfilePoint.
258:      *
259:      * @param string $message
260:      * Message to be logged.
261:      *
262:      * @return void
263:      *
264:      * @throws \Exception
265:      * It might throw an exception in the case of any errors:
266:      *
267:      * - if the profile file is not writable.
268:      *
269:      * @see DebugProfiler::startProfilePoint()
270:      *
271:      * @author Oleg Schildt
272:      */
273:     public function fixProfilePoint(string $message): void
274:     {
275:         if (!empty(self::$profile_time)) {
276:             $message = $message . ": " . number_format(microtime(true) - self::$profile_time, 3, ".", "") . " seconds";
277:         }
278: 
279:         $this->debugMessage($message, false, "profile.log");
280: 
281:         self::$profile_time = microtime(true);
282:     } // fixProfilePoint
283: 
284:     /**
285:      * Clears the specified log file.
286:      *
287:      * @param string $file_name
288:      * The target file name.
289:      *
290:      * @return void
291:      *
292:      * @see DebugProfiler::clearLogFiles()
293:      *
294:      * @author Oleg Schildt
295:      */
296:     public function clearLogFile(string $file_name): void
297:     {
298:         $file = $this->log_path . $file_name;
299: 
300:         if ((!file_exists($file) && is_writable($this->log_path)) || is_writable($file)) {
301:             @unlink($file);
302:         }
303:     } // clearLogFile
304: 
305:     /**
306:      * Clears all log files.
307:      *
308:      * @return void
309:      *
310:      * @see DebugProfiler::clearLogFile()
311:      *
312:      * @author Oleg Schildt
313:      */
314:     public function clearLogFiles(): void
315:     {
316:         $dir = $this->log_path;
317:         $files = scandir($dir);
318:         foreach ($files as $file) {
319:             if ($file == "." || $file == ".." || is_dir($dir . $file)) {
320:                 continue;
321:             }
322: 
323:             $pi = pathinfo($dir . $file);
324: 
325:             if (empty($pi["extension"]) || strtolower($pi["extension"]) != "log") {
326:                 continue;
327:             }
328: 
329:             $this->clearLogFile($file);
330:         }
331:     } // clearLogFiles
332: 
333:     /**
334:      * This is an auxiliary function that cuts off the common part of the path.
335:      *
336:      * @param string $path
337:      * The path.
338:      *
339:      * @return string
340:      * Returns the cut path.
341:      *
342:      * @author Oleg Schildt
343:      */
344:     protected function trim_path(string $path): string
345:     {
346:         $common_prefix = common_prefix(str_replace("\\", "/", __DIR__), $path);
347: 
348:         return str_replace($common_prefix, "", $path);
349:     } // trim_path
350: } // DebugProfiler