marko/log-file
File-based logging driver --- writes log messages to disk with daily or size-based rotation. This package provides FileLogHandler, a marko/log handler that appends formatted records to files. It supports daily rotation (one file per day) and size-based rotation (new file when the size limit is reached). The shared Logger in marko/log applies the level threshold and builds each record; this package only writes it. All settings come from config/log.php.
Installation
Section titled “Installation”composer require marko/log-fileThis automatically installs marko/log.
Configuration
Section titled “Configuration”The package adds the file handler to log.handler_map, which makes file a valid name in LOG_HANDLERS. file is the default, so installing the package is enough:
<?php
declare(strict_types=1);
use Marko\Log\File\Handler\FileLogHandler;
return [ 'handler_map' => [ 'file' => FileLogHandler::class, ],];The handler reads these marko/log settings:
| Key | Env | Default | Description |
|---|---|---|---|
path | LOG_PATH | storage/logs | Directory where log files are written |
max_file_size | LOG_MAX_FILE_SIZE | 10485760 | Size limit for SizeRotation (10 MB) |
file_mode | --- | 0600 | Mode applied to newly created log files |
dir_mode | --- | 0700 | Mode applied to a newly created log directory |
To write files and stream to stderr at the same time, install marko/log-stream too and set LOG_HANDLERS=file,stream. In containers, prefer LOG_HANDLERS=stream: files inside a container fill its ephemeral disk, and nothing collects them.
Inject LoggerInterface anywhere:
use Marko\Log\Contracts\LoggerInterface;
class ImportService{ public function __construct( private LoggerInterface $logger, ) {}
public function import( string $file, ): void { $this->logger->info('Starting import', ['file' => $file]); // ... $this->logger->info('Import complete', ['rows' => 1500]); }}Log output (with daily rotation): var/log/app-2025-01-15.log
[2025-01-15 10:30:00] app.INFO: Starting import {"file":"products.csv"}[2025-01-15 10:30:05] app.INFO: Import complete {"rows":1500}Rotation Strategies
Section titled “Rotation Strategies”Daily rotation (default) --- one file per day, date in filename:
var/log/app-2025-01-15.logvar/log/app-2025-01-16.logSize rotation --- rotates when a file exceeds the configured size limit (default: 10 MB):
var/log/app.logvar/log/app.1.logvar/log/app.2.logTime and Testing
Section titled “Time and Testing”The Logger stamps each record, and DailyRotation picks the day’s file, from the PSR-20 Psr\Clock\ClockInterface bound by marko/clock, never the system time directly. Freeze it with FakeClock to test record times and the midnight rollover exactly:
use Marko\Log\File\Handler\FileLogHandler;use Marko\Log\File\Rotation\DailyRotation;use Marko\Log\Formatter\LineFormatter;use Marko\Log\Logger;use Marko\Log\LogLevel;use Marko\Testing\Fake\FakeClock;
$clock = new FakeClock('2026-01-21 23:59:59');$logger = new Logger( handlers: [new FileLogHandler( path: $dir, formatter: new LineFormatter(), rotation: new DailyRotation($clock), )], channel: 'app', minimumLevel: LogLevel::Debug, clock: $clock,);
$logger->info('Before midnight'); // app-2026-01-21.log, [2026-01-21 23:59:59]$clock->travel('+1 second');$logger->info('After midnight'); // app-2026-01-22.log, [2026-01-22 00:00:00]Upgrading from 0.11
Section titled “Upgrading from 0.11”The logger moved into marko/log, and this package now provides only the file handler:
FileLoggerandFileLoggerFactoryare removed.LoggerInterfaceis bound bymarko/logtoMarko\Log\Logger, which sends records toFileLogHandler. Code that constructedFileLoggerdirectly builds aLoggerwith aFileLogHandler, as in the example above. A Preference or subclass ofFileLoggermoves toFileLogHandler(the write step) orLogger(threshold and record building).- This module no longer binds
LoggerInterface, so remove anyLoggerInterface::class => FileLogger::classbinding from yourmodule.php. log.driverandLOG_DRIVERare gone. Uselog.handlersandLOG_HANDLERS(defaultfile). A leftoverdriverkey in yourconfig/log.phpis ignored, so delete it.FileLogHandlernames the file after each record’s channel, so the constructor no longer takes$channel,$minimumLevelor$clock, and$rotationis required.
Customization
Section titled “Customization”The module binds RotationStrategyInterface to DailyRotation, resolved by the container, so the handler picks up a replacement. Replace the rotation strategy via Preference:
use Marko\Core\Attributes\Preference;use Marko\Log\File\Rotation\DailyRotation;use Marko\Log\File\Rotation\SizeRotation;
#[Preference(replaces: DailyRotation::class)]class CustomRotation extends SizeRotation{ public function __construct() { parent::__construct(maxSize: 50 * 1024 * 1024); // 50 MB }}API Reference
Section titled “API Reference”FileLogHandler
Section titled “FileLogHandler”Implements LogHandlerInterface from marko/log.
use Marko\Log\LogRecord;
public function handle(LogRecord $record): void;The constructor accepts the following parameters:
| Parameter | Type | Description |
|---|---|---|
$path | string | Directory where log files are written (log.path) |
$formatter | LogFormatterInterface | Formats log records into strings (log.formatter) |
$rotation | RotationStrategyInterface | Picks the file for each record’s channel |
$fileMode | int | Mode applied to newly created log files (default 0600, from log.file_mode) |
$dirMode | int | Mode applied to a newly created log directory (default 0700, from log.dir_mode) |
Writes use FILE_APPEND | LOCK_EX for safe concurrent appends. The log directory is created automatically if it does not exist, with $dirMode. Each new log file is created empty and chmod-ed to $fileMode before the first record is written, so log content is never readable by other local users under a permissive umask. Existing files and directories keep their current permissions. Set log.file_mode to 0640 and log.dir_mode to 0750 if a log shipper running as a group member needs read access. A LogWriteException is thrown if the directory is not writable, the mode cannot be applied, or a write fails.
RotationStrategyInterface
Section titled “RotationStrategyInterface”use Marko\Log\File\Rotation\RotationStrategyInterface;
interface RotationStrategyInterface{ public function getCurrentPath(string $basePath, string $channel): string; public function needsRotation(string $filePath): bool;}Built-in Rotation Strategies
Section titled “Built-in Rotation Strategies”DailyRotation--- Date-stamped filenames from the injectedClockInterface, rotates automatically each daySizeRotation--- Numbered filenames, rotates when file exceeds max size (default: 10 MB)
Related Packages
Section titled “Related Packages”marko/log--- the logger, handler contract and formattersmarko/log-stream--- stderr/stdout handler for containers, installable alongside this one