Error Handling
One handler catches everything and decides what the caller sees. That is why you throw instead of building error responses, and why a silent failure is almost always a caught one.
Overview
Startup installs handlers for PHP errors, uncaught exceptions and fatal shutdowns. From that point nothing fails on its own terms. The handler records it, then shows it in the shape the request expects, an HTML page or a JSON body.
Inside an operation there is a second layer. The dispatcher wraps your method in a try block. An exception you throw becomes the error response, with your message as its text. That is the whole error path for a mutation: throw, and stop writing.
Reference
Raising a Failure
$error property and returns false is carrying a pattern from the previous major version. The base classes still translate that into a throw, but new code does not write it.
Recording
public static function getInstance(): MioException;
public function initialize(): void;
public function logError(string $message, array $context = [], string $level = Logger::LEVEL_ERROR, string $type = Logger::TYPE_SYSTEM): string;
public function logDatabaseError(string $query, string $error, array $params = []): string;
public function sanitizeData(array $data, int $depth = 0): array; // $depth is internal recursion; pass nothing
public static function showErrors(): void;
public static function getInstance(): Logger;
public function log(string $level, string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
// One static per level, all with the same shape.
public static function debug(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function info(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function warning(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function error(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function fatal(string $message, array $context = [], string $type = self::TYPE_SYSTEM): string;
public static function database(string $query, string $error, array $params = []): string;
// Runs the callback, records anything it throws and returns null instead of rethrowing.
public static function safe(callable $callback, array $context = [], string $errorMessage = 'Operation failed');
initialize().
['query' => ..., 'parameters' => ...].
null. That identifier is what a support request arrives with. It takes you from the customer's message to the stored context.
Levels and Types
Both are plain strings, and the constants are the only spelling that is safe to write. Every recording call returns the identifier as a string. It is uppercase hexadecimal padded to at least six characters, followed by three digits, so 4F59C9BC644 rather than a fixed width. Store it as text and do not validate its length.
| Constant | Value | Use it for |
|---|---|---|
Logger::LEVEL_DEBUG | DEBUG | Development tracing, not for a shipped path. |
Logger::LEVEL_INFO | INFO | Something happened that someone may want to confirm later. |
Logger::LEVEL_WARNING | WARNING | Recovered, degraded, retried. |
Logger::LEVEL_ERROR | ERROR | The default. The operation did not do what it was asked. |
Logger::LEVEL_FATAL | FATAL | The request cannot continue. Written by the shutdown handler. |
Logger::LEVEL_DEPRECATED | DEPRECATED | Engine deprecation notices, kept apart so they can be filtered out. |
Logger::TYPE_SYSTEM | system | The default type, everything that is not a query. |
Logger::TYPE_DATABASE | database | Query failures. Kept in a separate log so one noisy query cannot bury the rest. |
What Redaction Actually Matches
A key is redacted when its lowercased name contains any of password, pass, passwd, pwd, secret, token, key, auth, api_key or private_key. It is a substring test, applied recursively to nested arrays, and it looks at keys only.
$in = [
'email' => '[email protected]',
'Password' => 's3cr3t', // matched case insensitively
'api_token' => 'abc123', // contains "token"
'keyword' => 'hosting', // contains "key" - redacted too
'server' => ['auth_user' => 'root', 'port' => 22],
];
$out = MioException::getInstance()->sanitizeData($in);
// [
// 'email' => '[email protected]',
// 'Password' => '***REDACTED***',
// 'api_token' => '***REDACTED***',
// 'keyword' => '***REDACTED***',
// 'server' => ['auth_user' => '***REDACTED***', 'port' => 22],
// ]
What the Caller Sees
| Situation | Response |
|---|---|
| Exception thrown in an operation | {"status":"error","message":"<your message>"} |
| Error caught by the handler during an AJAX request | {"status":"error","message":"Error ID: <id>: <text>","error_id":"<id>"}, plus a debug member when diagnostics are on |
| Fatal during a page | The error page; detail only when diagnostics are on |
| Non-fatal warning | Recorded; visible on the page only when diagnostics are on |
| Anything on the command line | Recorded; the message can be swallowed by the output buffer |
Example
// In an operation: throw, and let the dispatcher answer.
public function delete_widget(Operation $operation): bool
{
$operation->demo();
$id = (int) Filter::init("POST/id", "rnumbers");
if (!$id) throw new Exception(Language::gc("widgets/error-id-required"));
if (!$this->model->delete($id)) throw new Exception(Language::gc("widgets/error-delete-failed"));
return $operation->output(['status' => "successful"]);
}
// In a module: throw. The caller catches it and surfaces the message.
public function create(): array|false
{
$response = $this->api->create($params);
if (!($response['success'] ?? false))
throw new Exception((string) ($response['message'] ?? $this->lang["err-provider"]));
return ['status' => 'SUCCESS'];
}
// Recording something you handled yourself. Redact first; the identifier that
// comes back is the one to quote in the message the operator will read.
$mio = MioException::getInstance();
$id = $mio->logError("Provider refused the transfer", $mio->sanitizeData($_POST), Logger::LEVEL_WARNING);
throw new Exception(Language::gc("services/error-transfer", ['{id}' => $id]));
// The browser then receives, from the operation dispatcher:
// {"status":"error","message":"Transfer failed. Reference: 4F59C9BC644"}
//
// Reading it back on the command line:
// php coremio/errlog.php list --limit=10
// php coremio/errlog.php show <signature>
Pitfalls
logError() starts with the message, logDatabaseError() starts with the query and takes the message second, and Logger::log() starts with the level. Swapping the first two in the database call is silent. You get a log entry whose message is a SQL statement, and whose recorded query is an error text.
The handler records non-fatal warnings too, and each record is a write. A missing array key read inside a loop costs one write per iteration. That is why values are read with a default rather than hopefully.
The log is a file that outlives the request and gets copied around. Redaction is a substring test on key names, so it is generous in one direction and blind in the other. A field named card or answer passes through untouched. Do not pass raw request data and assume the list covers your field.
Nothing on screen means the handler decided not to show it, not that nothing happened. Read the log before concluding that a code path was never reached.
Related Articles
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.