Logger
StaticPHP\Core\Models\Logger collects messages in memory for the duration of a request
and renders them as a debug block at the end. It does not write to disk, does not open a
socket and has no handler or formatter abstraction - Logger::$logs is a protected array
that grows until the process exits.
It is PSR-3 shaped without being PSR-3: the eight level methods have the familiar names and
(string $message, array $context = []) signatures, but they are static, there is no
interface, and $context is not interpolated into the message.
<?php
namespace StaticPHP\Core\Models;
class Logger{ public const ERROR_LEVELS = [...];
public const NONE = 'none'; public const EMERGENCY = 'emergency'; public const ALERT = 'alert'; public const CRITICAL = 'critical'; public const ERROR = 'error'; public const WARNING = 'warning'; public const NOTICE = 'notice'; public const INFO = 'info'; public const DEBUG = 'debug';
public static function above(string $eventLevel, string $currentLevel): bool; public static function below(string $eventLevel, string $currentLevel): bool;
public static function emergency(string $message, array $context = []): void; public static function alert(string $message, array $context = []): void; public static function critical(string $message, array $context = []): void; public static function error(string $message, array $context = []): void; public static function warning(string $message, array $context = []): void; public static function notice(string $message, array $context = []): void; public static function info(string $message, array $context = []): void; public static function debug(string $message, array $context = []): void; public static function log(string $level, $message, array $context = []): void;
public static function debugOutput(): string;}Levels
Section titled “Levels”Logger::ERROR_LEVELS maps each level name to a number. The scale runs downwards -
more severe means a larger number, and none sits above everything:
| Constant | Name | Value |
|---|---|---|
NONE |
none |
1000 |
EMERGENCY |
emergency |
800 |
ALERT |
alert |
700 |
CRITICAL |
critical |
600 |
ERROR |
error |
500 |
WARNING |
warning |
400 |
NOTICE |
notice |
300 |
INFO |
info |
200 |
DEBUG |
debug |
100 |
The eight level methods all delegate to log(), which appends
['level' => ..., 'message' => ..., 'context' => ...] to the array and does nothing else -
no threshold is applied at write time. Every message logged during a request is retained;
the levels only matter when something reads them back.
above() and below()
Section titled “above() and below()”<?php
public static function above(string $eventLevel, string $currentLevel): bool;public static function below(string $eventLevel, string $currentLevel): bool;above() returns ERROR_LEVELS[$eventLevel] >= ERROR_LEVELS[$currentLevel], and below()
is its <= mirror. Read above() as “this event is at or over the configured threshold”,
which is how the error handlers use it:
<?php
if (Logger::above('error', sp_logging_level('log_level'))) { sp_log_error($e);}The parameter order is the point of the naming. Both arguments are level names and both are strings, so getting them the wrong way round was silent - the event being reported goes first, the configured threshold second.
A lower configured threshold is the more inclusive one. 'debug' (100) admits errors;
'error' (500) admits errors exactly; 'emergency' (800) does not - it admits nothing
below emergency. 'none' (1000) admits nothing at all, which is what makes it the off
switch.
An unknown level name on either side returns false rather than raising. That is
deliberate: a mistyped level in the configuration used to produce a TypeError from inside
the error handler, which replaced the failure being reported. A name the class does not
know means “do not log at this level”.
The three thresholds the error handlers consult - $config['logging']['display_level'],
['log_level'] and ['report_level'] - are read through sp_logging_level(), which falls
back to Logger::ERROR for a missing or non-string value. See
errors.
debugOutput()
Section titled “debugOutput()”<?php
public static function debugOutput(): string;Calls Timers::logTimers() first, which appends the execution time, the memory used and
every recorded timer as info entries, and then renders every collected message as a line
of html:
<span class="text-info">INFO: </span>Total execution time: 0.00412 seconds;The class on the span is danger for emergency, alert and critical; warning for error
and warning; info for notice, info and debug - bootstrap-style utility class names, so
the block picks up whatever the surrounding stylesheet defines. A non-empty $context is
appended as [a,b,c], imploded with ,, so context values need to be strings or things
that cast cleanly to one.
Each line ends with \n and nothing wraps the block, so a template decides where it goes:
{% if config.debug %} <pre class="debug">{{ debugOutput()|raw }}</pre>{% endif %}The bootstrap registers debugOutput as a twig function when the view engine is built. In
a plain php view, call Logger::debugOutput() directly.
Calling it twice calls logTimers() twice, which appends a second set of timing entries to
the same array - so the second call prints everything from the first plus the new lines.
Render it once, at the end.
An example
Section titled “An example”<?php
use StaticPHP\Core\Models\Logger;
Logger::info('Cache miss for report', ['monthly', '2026-07']);
try { $db->query($sql);} catch (\PDOException $e) { Logger::error('Report query failed: ' . $e->getMessage());}Neither line goes anywhere on its own. They appear when debugOutput() is rendered, which
in practice means when debug mode is on and the layout includes the block. For a message
that has to survive the request, use error_log(), or throw - the exception handler routes
through sp_log_error() and sp_send_error_email().