-
Notifications
You must be signed in to change notification settings - Fork 1
Custom Handlers
InitPHP\Logger\Logger accepts any Psr\Log\LoggerInterface. Adding new
backends — syslog, Slack, an in-memory ring buffer for tests, a third-party
SaaS — is simply a matter of writing a class that implements that contract.
The path of least resistance is to extend Psr\Log\AbstractLogger: it
provides default implementations of emergency() through debug() that all
delegate to a single log() method. You only implement log().
use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
final class SyslogHandler extends AbstractLogger
{
private const PRIORITY = [
LogLevel::EMERGENCY => LOG_EMERG,
LogLevel::ALERT => LOG_ALERT,
LogLevel::CRITICAL => LOG_CRIT,
LogLevel::ERROR => LOG_ERR,
LogLevel::WARNING => LOG_WARNING,
LogLevel::NOTICE => LOG_NOTICE,
LogLevel::INFO => LOG_INFO,
LogLevel::DEBUG => LOG_DEBUG,
];
public function __construct(string $ident = 'app', int $facility = LOG_USER)
{
openlog($ident, LOG_PID | LOG_ODELAY, $facility);
}
public function log($level, string|\Stringable $message, array $context = []): void
{
$normalised = strtolower((string) $level);
if (!isset(self::PRIORITY[$normalised])) {
throw new InvalidArgumentException(sprintf('Unknown log level "%s".', $level));
}
syslog(self::PRIORITY[$normalised], $this->interpolate((string) $message, $context));
}
private function interpolate(string $message, array $context): string
{
if ($context === []) {
return $message;
}
$replace = [];
foreach ($context as $key => $value) {
if (is_scalar($value) || $value instanceof \Stringable) {
$replace['{' . $key . '}'] = (string) $value;
}
}
return strtr($message, $replace);
}
}Wire it into the multiplexer alongside the built-in handlers:
use InitPHP\Logger\FileLogger;
use InitPHP\Logger\Logger;
$logger = new Logger(
new FileLogger(['path' => __DIR__ . '/logs/app.log']),
new SyslogHandler('myapp', LOG_USER)
);Capturing log records in memory makes assertions trivial.
use InitPHP\Logger\HelperTrait;
use Psr\Log\AbstractLogger;
final class ArrayLogger extends AbstractLogger
{
use HelperTrait;
/**
* @var list<array{level: string, message: string, context: array<string, mixed>, date: string}>
*/
public array $records = [];
public function log($level, string|\Stringable $message, array $context = []): void
{
$this->logLevelVerify($level);
$this->records[] = [
'level' => strtoupper((string) $level),
'message' => $this->interpolate($message, $context),
'context' => $context,
'date' => $this->getDate('c'),
];
}
}ArrayLogger reuses the package's own HelperTrait for context
interpolation and level validation. The trait is marked @internal, so
treat it as an implementation detail — but it is shipped in src/ and
covered by tests like the rest of the package. See
Testing Your Logging for usage examples.
use Psr\Log\AbstractLogger;
use Psr\Log\LogLevel;
final class SlackHandler extends AbstractLogger
{
private const NOTIFY = [
LogLevel::EMERGENCY => true,
LogLevel::ALERT => true,
LogLevel::CRITICAL => true,
LogLevel::ERROR => true,
];
public function __construct(private string $webhookUrl) {}
public function log($level, string|\Stringable $message, array $context = []): void
{
$key = strtolower((string) $level);
if (!($this::NOTIFY[$key] ?? false)) {
return; // Slack only cares about high-severity events.
}
$payload = json_encode([
'text' => sprintf('[%s] %s', strtoupper((string) $level), (string) $message),
], JSON_THROW_ON_ERROR);
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => $payload,
'timeout' => 2,
'ignore_errors' => true,
],
]);
@file_get_contents($this->webhookUrl, false, $ctx);
// Network failures swallowed on purpose — see "Guidelines" below.
}
}A handful of rules to keep your handler well-behaved as a PSR-3 citizen.
Either directly, or — preferably — by extending
\Psr\Log\AbstractLogger. Anything else will not fit into
InitPHP\Logger\Logger.
PSR-3 §1.1 explicitly permits one exception type from log():
\Psr\Log\InvalidArgumentException, for unknown levels. Everything else —
network errors, full disks, database outages, JSON encoding failures — must
either be swallowed (with an error_log() notice) or routed to a
fallback handler. A logger that throws on every call interrupts business
logic that is supposed to be observing failures, not creating them.
Users expect {exception} to produce something readable. Either reuse the
package's HelperTrait::interpolate() or special-case
instanceof \Throwable yourself.
PSR-3 v3 requires it. Copy the log() signature exactly:
public function log($level, string|\Stringable $message, array $context = []): voidPHP's type system will enforce the second parameter for you.
A misconfigured handler should fail at boot, not on the first log call —
matching the behaviour of the built-in handlers. Use \InvalidArgumentException
from the constructor and a clear, actionable error message:
throw new \InvalidArgumentException(
'SyslogHandler "ident" option must be a non-empty string.'
);Handlers run on every call. Avoid expensive work in the hot path: cache prepared statements, lazy-open files, batch network calls when the payload allows.
- Multi-Logger — composing custom handlers with the built-ins.
- PSR-3 Compliance — what the contract demands.
- Context Interpolation — placeholder rules to honour in your own handler.
-
Testing Your Logging —
ArrayLogger-style patterns.
initphp/logger · MIT License · part of the InitPHP family
Source · Issues · Discussions · Packagist · Contributing · Security Policy
Getting Started
Handlers
PSR-3 Behaviour
Practical Guides
Reference