Files
xamxam/app/src/Logger.php
Pontoporeia 99125cc8e3 Add autosave draft system for partage form with HTMX-based session persistence
- New fragment endpoint POST/GET /partage/fragments/draft.php:
  saves all form fields to PHP session, excludes file/csrf/slug fields
  GET returns JSON for JS hydration on page load
  rotates both global CSRF and share CSRF tokens in sync

- form.php accepts optional $formExtraAttrs and $showAutosaveStatus:
  allows injecting HTMX attributes and 'Brouillon enregistré' indicator

- renderShareLinkForm adds hx-post with change/input debounce trigger,
  loads autosave-handler.js, hydrate fields from draft on page load

- Draft cleared on successful form submission in handleShareLinkSubmission

- autosave-handler.js now also updates share_link_token hidden input
  when rotating CSRF token (partage form uses both csrf_token and share_link_token)

- Added .autosave-status CSS to form.css (was admin.css-only)

- Updated fragment routing to accept GET requests (needed for draft hydration)
2026-06-11 11:04:49 +02:00

106 lines
3.2 KiB
PHP

<?php
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\NullHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Level;
use Psr\Log\LoggerInterface;
/**
* Central logger factory — provides named Monolog channel instances
* backed by rotating JSON-line log files.
*
* Channels:
* - 'app' → replaces AppLogger
* - 'admin' → replaces AdminLogger file output
* - 'error' → replaces ErrorHandler logging
* - 'audit' → replaces Audit file shadow (DB writes stay in Audit)
*
* Usage:
* Logger::get('app')->info('message', [...]);
* Logger::get('admin')->warning('message', [...]);
*/
class Logger
{
/** @var array<string, LoggerInterface> */
private static array $channels = [];
/**
* Get (or lazily create) a named Monolog channel.
*/
public static function get(string $channel): LoggerInterface
{
if (!isset(self::$channels[$channel])) {
self::$channels[$channel] = self::create($channel);
}
return self::$channels[$channel];
}
/**
* Create a Monolog channel with rotating JSON file handler.
*
* Falls back to NullHandler if the log directory is not writable
* (e.g. CLI scripts on a machine that doesn't have the production path).
*/
private static function create(string $channel): \Monolog\Logger
{
$logDir = self::logDir();
if (!is_dir($logDir) && !@mkdir($logDir, 0755, true) && !is_dir($logDir)) {
// Directory can't be created — use null handler (graceful degradation)
$logger = new \Monolog\Logger($channel);
$logger->pushHandler(new NullHandler());
return $logger;
}
try {
$handler = new RotatingFileHandler(
$logDir . '/' . $channel . '.log',
30, // keep 30 days of logs
self::level()
);
} catch (\Throwable $e) {
// Can't open log file — use null handler
$logger = new \Monolog\Logger($channel);
$logger->pushHandler(new NullHandler());
return $logger;
}
// Pass-through formatter: the facades (AppLogger, AdminLogger, etc.)
// construct their own JSON lines and pass them as the log message.
// %message% preserves the existing JSON format contract exactly.
$handler->setFormatter(new LineFormatter("%message%\n", null, true));
$logger = new \Monolog\Logger($channel);
$logger->pushHandler($handler);
return $logger;
}
/**
* Read the LOG_LEVEL env var with sensible defaults.
*/
private static function level(): Level
{
$level = strtoupper(getenv('LOG_LEVEL') ?: '');
// Default: WARNING in production (always set in .env), DEBUG otherwise
if ($level === '') {
return php_sapi_name() === 'cli-server' ? Level::Debug : Level::Warning;
}
return Level::fromName($level);
}
/**
* Resolve the log directory.
*/
private static function logDir(): string
{
if (defined('STORAGE_ROOT')) {
return STORAGE_ROOT . '/logs';
}
return __DIR__ . '/../storage/logs';
}
}