Purpose
Scriptor's default PSR-3 logger. One file, one line per record,
greppable with the usual tools. Bound into the DI container as
Psr\Log\LoggerInterface so theme code ($site->logger),
editor code ($editor->logger), and plugin code all reach the
same sink.
Intentionally small. Records below the configured min level are
dropped silently; nothing else is filtered, decorated, or routed.
Swap the binding in boot.php for Monolog or any PSR-3
implementation when you need rotation, multiple sinks, or
structured output.
FQCN + file path
- FQCN:
Scriptor\Boot\Logging\FileLogger - Source:
boot/Logging/FileLogger.php
When to use
You almost never touch FileLogger directly. The shape every
caller uses is the PSR-3 interface:
$this->logger->info('Contact form submitted from {email}', [
'email' => $email,
]);
Themes have it on $site->logger, the editor surface has it on
$editor->logger, plugins resolve it from the container:
$logger = $context->container()->get(\Psr\Log\LoggerInterface::class);
Direct construction of FileLogger is for two cases:
- Swapping the binding in
boot.phpto point at a different file or min level than the configured defaults. - A standalone script (CLI tool, cron job) that runs outside the request lifecycle and wants logs in the same file.
Surface
Constructor
public function __construct(
private readonly string $path,
string $minLevel = LogLevel::INFO,
)
$pathis the absolute file path. The parent directory is created lazily on the first write, sodata/logs/does not have to exist beforehand.$minLevelis a PSR-3 level constant (Psr\Log\LogLevel::DEBUG,INFO,NOTICE,WARNING,ERROR,CRITICAL,ALERT,EMERGENCY). Records below this level are dropped without going through the format step. Unknown level strings raiseInvalidArgumentExceptionat construction time.
Logging methods
Inherited from Psr\Log\AbstractLogger (and therefore the
PSR-3 LoggerInterface):
public function emergency(string|\Stringable $message, array $context = []): void
public function alert (string|\Stringable $message, array $context = []): void
public function critical (string|\Stringable $message, array $context = []): void
public function error (string|\Stringable $message, array $context = []): void
public function warning (string|\Stringable $message, array $context = []): void
public function notice (string|\Stringable $message, array $context = []): void
public function info (string|\Stringable $message, array $context = []): void
public function debug (string|\Stringable $message, array $context = []): void
public function log(mixed $level, string|\Stringable $message, array $context = []): void
All eight level shortcuts forward to log(), which is the only
method FileLogger overrides.
Configuration
The default container binding reads from scriptor-config.php:
return [
// ...
'logging' => [
'path' => __DIR__ . '/data/logs/scriptor.log',
'level' => 'info', // PSR-3 LogLevel constant string
],
];
Both keys are optional. The wiring in boot.php falls back to
data/logs/scriptor.log + info if either is missing.
Output shape
One line per record, Monolog-ish on purpose so the same grep
patterns work across Scriptor and Monolog-using sites:
[2026-05-22T14:23:00+02:00] INFO: Contact form submitted from juri@example.com
The timestamp is ISO 8601 (date('c')), the level is uppercased,
the message is post-interpolation.
Placeholder interpolation (PSR-3 §1.2)
{key} tokens in the message are substituted from
$context[key]:
$logger->info('user {id} edited page {slug}', [
'id' => 42,
'slug' => 'home',
]);
// -> [2026-...] INFO: user 42 edited page home
Substitution rules:
$context[key] value |
Becomes |
|---|---|
null, scalar, or Stringable |
(string) $value |
Throwable |
$value->getMessage() (NOT the stack trace) |
| Anything else (array, non-stringable object) | Untouched; the literal {key} stays in the message |
The Throwable-special-case is deliberate: a Throwable's
__toString() returns the full stack trace, which would explode
one log line into hundreds. Loggers want the message; downstream
handlers that want the trace read it from
$context['exception'] per PSR-3 §1.3.
Lifecycle
Container-bound singleton, request-scoped (one per request, one
shared instance across every caller). Constructed once by the
container factory; the file descriptor is not held open between
records (every write opens, appends, closes via
file_put_contents with FILE_APPEND | LOCK_EX).
Concurrency is handled by LOCK_EX: PHP-FPM workers writing the
same file serialise correctly. There is no record buffer; every
call hits the filesystem. For high-volume logging move to a
real PSR-3 implementation.
Common patterns
Plain info log with placeholders
$site->logger->info('payment {id} authorised for {amount}', [
'id' => $paymentId,
'amount' => number_format($amount, 2),
]);
Logging a caught exception with context
try {
$this->processor->run($file);
} catch (\Throwable $e) {
$site->logger->error('processing {file} failed: {error}', [
'file' => $file->name,
'error' => $e, // -> getMessage() inline
'exception' => $e, // -> downstream handlers can read the trace
]);
}
Lifting min level temporarily in scriptor-config.php
'logging' => [
'path' => __DIR__ . '/data/logs/scriptor.log',
'level' => 'debug', // include debug + info for a debugging session
],
Swapping in Monolog (rotation, multiple handlers)
// In boot.php, after the default binding:
$container->add(
\Psr\Log\LoggerInterface::class,
static function () {
$log = new \Monolog\Logger('scriptor');
$log->pushHandler(new \Monolog\Handler\RotatingFileHandler(
__DIR__ . '/data/logs/scriptor.log',
14, // keep 14 days
));
return $log;
},
);
The site continues to call $site->logger->info(...); only the
implementation changes.
See also
Site: holds the logger as$site->loggerEditor: holds the logger as$editor->loggerPluginContext: how a plugin reaches it fromregister()viacontainer()->get(LoggerInterface::class)- Build a Theme: Forms, Errors, Publishing:
walks
$site->loggeragainst a real contact-form flow - PSR-3 LoggerInterface
for the inherited surface (
emergency,alert, …debug, message interpolation rules)