zhamao/logger
zhamao/logger is a lightweight logging library for PHP applications, offering configurable log levels, file and console output, and simple integration with Zhamao-based projects. Use it to write, format, and manage application logs with minimal setup.
zhamao/logger is a standalone, PSR-3 console logger aimed at long-running CLI processes (the kind of thing you'd run under Swoole in the Zhamao robot framework), not an HTTP-side logging library. Treat it as a replacement for echo/dump()/Symfony Console styling in commands and workers, not as a replacement for Laravel's Log facade.
Minimal setup:
composer require zhamao/logger
There is no service provider and no extra.laravel auto-discovery — the package is framework-agnostic. So the first thing to do is open src/ and read the Logger class constructor. You're looking for:
Psr\Log\LoggerInterface (if it does, everything below gets easier).fwrite(STDOUT, ...), echo, or a stream you can inject.First use case, and the one that justifies the dependency: an artisan command that currently spams $this->info() / $this->line() calls.
use Zhamao\Logger\Logger;
class SyncFeedCommand extends Command
{
protected $signature = 'feed:sync {--v=0}';
public function handle(): int
{
$logger = new Logger(/* min level from --v */);
$logger->info('Starting feed sync', ['source' => 'crm']);
try {
// ...
$logger->notice('Sync completed', ['rows' => $count]);
return self::SUCCESS;
} catch (\Throwable $e) {
$logger->error('Sync failed: {message}', ['message' => $e->getMessage(), 'exception' => $e]);
return self::FAILURE;
}
}
}
If it's not PSR-3 compliant, wrap it in a five-line adapter class before you use it anywhere else — you'll want that seam later.
Pin the dependency ("zhamao/logger": "1.1.9" or ^1.1.9) and read the changelog before bumping; see the risk notes below.
Wrap it behind an interface, always. The whole value of this package is presentation (colour, level filtering, timestamps). Consumption sites should only ever type-hint Psr\Log\LoggerInterface. That way a future swap to Monolog costs you nothing.
$this->app->singleton(LoggerInterface::class, fn () => new Logger(...));
Register it as a custom Laravel log channel if you want Log::channel('cli')->info(...) for consistency with the rest of the app. You'll write a tiny driver that implements Illuminate\Log\Logger's expectations or simply extends Monolog's AbstractProcessingHandler and delegates to the zhamao logger. For most teams the cheaper route is: don't touch the channel config, inject the logger directly into CLI classes and leave Log alone for HTTP.
Map verbosity to levels. Console apps live and die by -v/-vv/-vvv. Resolve the option once in handle()/boot() and hand the resulting minimum level to the logger constructor. Don't sprinkle if ($this->option('verbose')) guards through the codebase.
Use structured context, not string concatenation. Pass arrays and use {placeholder} interpolation where supported. $logger->info('Job dispatched', ['id' => $id, 'queue' => $q]) survives a switch to JSON output; "Job $id dispatched to $q" does not.
Test against Psr\Log\NullLogger. Because you're injecting LoggerInterface, feature tests of commands need no special handling — bind NullLogger::class in the test container and assert on outcomes, not on printed text. If you do need to assert on output, back the logger with php://memory rather than capturing real STDOUT.
Long-running workers: instantiate one logger per process, not per job. Keep a monotonically increasing request/job ID in the context so interleaved lines remain traceable.
Since 1.1.9, writes are no longer @-suppressed. The library now lets PHP surface failures from echo/fwrite/fflush. That is the correct behaviour, but it means a closed or broken output stream (artisan ... | head -n 20, a rotated/short-lived log fifo, a supervisor that closed the pipe) can now produce fwrite(): Write of N bytes failed / broken-pipe warnings in your output or error handler instead of failing silently. If you run commands in pipes, either tolerate the warning at the end of the run or wrap your top-level handle() so a failed final flush doesn't turn a successful command into a non-zero exit. Test your command under | head -1 once after upgrading — it's a two-minute check that catches this.
composer.json, and verify the release cadence against your PHP version before adopting. The PSR-3 wrapper discipline above is your insurance policy.head, writing to a closed stream in a dying worker, disk-full on a redirected STDOUT) now emits PHP warnings from fwrite/fflush. If you have a custom set_error_handler, make sure it doesn't convert these E_WARNINGs into exceptions that kill a worker mid-job. Check the release diff (zhamao-robot/zhamao-logger PR #10) if you were on 1.1.8 or earlier.storage/logs. Keep it in artisan commands, queue workers, and standalone scripts.| tee, or a supervisor-managed logfile. Check posix_isatty(STDOUT) (or stream_isatty) at boot and disable colour when it returns false. Many CLI loggers expose a setColor(false) or accept it in the constructor — find it early, because retrofitting it after your deployment pipeline is full of ^[[32m is unpleasant.fwrite of a fully-formed string. Note that with suppression removed, a partially-failed write is now visible — which is useful for diagnosing interleaving, but noisier.Logger::getInstance(), be aware of it in tests — state persists across test methods and level changes bleed between them. Prefer constructor injection and reset any static handle in tearDown.storage/logs/laravel.log and your console. Pick one path per environment: console logger in CLI, Monolog everywhere else.Psr\Log\AbstractLogger inheritance or an overridable format()/write() method. That's where you add JSON lines for log shippers, or strip timestamps when your process supervisor already adds them. If you override write(), preserve the new un-suppressed error behaviour rather than reintroducing @.success, debug with custom weights). A quick grep through the source beats guessing, and a wrong level passed to a strict PSR-3 implementation throws InvalidArgumentException at runtime rather than silently downgrading.How can I help you explore Laravel packages today?