Custom client-facing error message * 'log' => Custom server log label * 'code' => HTTP response code (default: 500) * 'before_exit' => Callback function before exit * * @example * // Simple — use all defaults * db_check($sth, $answer); * * @example * // Custom client message * db_check($sth, $answer, [ * 'message' => "Contact not found." * ]); * * @example * // Custom log label + message * db_check($sth, $answer, [ * 'log' => "Failed to fetch contact", * 'message' => "Could not load contact. Please try again." * ]); * * @example * // Custom HTTP status code * db_check($sth, $answer, [ * 'code' => 403, * 'message' => "Access denied." * ]); * * @example * // Cleanup callback before exit * db_check($sth, $answer, [ * 'message' => "Upload failed.", * 'before_exit' => function() use ($tmp_file) { * if(file_exists($tmp_file)) unlink($tmp_file); * } * ]); */ function db_check($sth, &$answer, $options = []) { if ($sth->errorInfo()[0] != "00000" && !empty($sth->errorInfo()[0])) { $trace = debug_backtrace()[0]; $log_message = $options['log'] ?? "DB Error"; error_log($log_message . " in " . $trace['file'] . " line " . $trace['line'] . ": " . $sth->errorInfo()[2]); // production version $answer["message"] = $options['message'] ?? "A server error occurred. Please try again."; http_response_code($options['code'] ?? 500); if(isset($options['before_exit'])) { call_user_func($options['before_exit']); } exit(json_encode($answer)); } } // Run a callback after the surrounding dbTransaction() commits. If no managed // transaction is active, run immediately. Used for non-authoritative side effects // such as live dashboard notifications. function db_after_commit(callable $callback): void { if (!isset($GLOBALS['_db_after_commit_stack']) || empty($GLOBALS['_db_after_commit_stack'])) { $callback(); return; } $idx = count($GLOBALS['_db_after_commit_stack']) - 1; $GLOBALS['_db_after_commit_stack'][$idx][] = $callback; } // transaction wrapper with retry logic for deadlocks function dbTransaction($pdo, $callback, $maxRetries = 3) { for ($attempt = 0; $attempt < $maxRetries; $attempt++) { $GLOBALS['_db_after_commit_stack'][] = []; try { $pdo->beginTransaction(); $result = $callback($pdo); $pdo->commit(); $callbacks = array_pop($GLOBALS['_db_after_commit_stack']); foreach ($callbacks as $after_commit) { try { $after_commit(); } catch (Throwable $e) { error_log("[AFTER COMMIT ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); } } return $result; } catch (PDOException $e) { if ($pdo->inTransaction()) $pdo->rollBack(); array_pop($GLOBALS['_db_after_commit_stack']); if ($e->getCode() == '40001' && $attempt < $maxRetries - 1) { usleep(600000 * ($attempt + 1)); continue; } error_log("[DB ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); throw $e; } catch (Exception $e) { if ($pdo->inTransaction()) $pdo->rollBack(); array_pop($GLOBALS['_db_after_commit_stack']); error_log("[ERROR] " . $e->getMessage() . " | " . $e->getFile() . ":" . $e->getLine()); throw $e; } } } ?>