Error Handling
This discusses the tools made available by your programming language to find bugs in your code. It also talks about conventions and best practices for writing reliable, performant and fault-tolerant applications.
- Performance pitfalls are identified here and work-arounds given.
- Testing talks about all the tools your programming language offers you for testing code, including examples of how it is done.
This section covers:
- How Dorguzen catches errors and exceptions (development vs production)
- The
DGZ_Exceptionclass and its type constants - Production error alerts
- Validation errors and custom exception handling
- The awesome
dgzie()helper function - Writing custom logs for your application
- Creating custom logs with
DGZ_Logger- Dorguzen Logging System — Full Documentation
- The difference between PHP's
error_log()andDGZ_Logger - How to write values to console in PHPUnit tests
Development vs Production
Dorguzen catches PHP errors and unhandled exceptions, logs them, and renders an appropriate response — detailed in development, generic in production. The behaviour is controlled by APP_ENV in .env, which the framework reduces to the config('live') flag (configs/app.php).
APP_ENV |
config('live') |
Error display |
|---|---|---|
local |
'false' |
Full exception details (dump with file, line, hint and trace) |
prod / production |
'true' |
Generic message; details logged but never shown |
Set in .env:
APP_ENV=local # development
APP_ENV=prod # production
In local, bootstrap/helpers.php turns on display_errors and error_reporting(E_ALL). In production it disables display, enables log_errors, and writes to a PHP error log file. In production, full exception details are logged but never shown to the user.
How Exceptions Flow
PHP error / thrown exception
↓
set_error_handler() (bootstrap/helpers.php)
→ Converts PHP errors to DGZ_Exception (typed error constant + hint)
→ On production (config('live') === 'true'):
emails the admin via DGZ_Messenger::sendErrorLogMsgToAdmin()
→ new ExceptionController()->redirect('exception', 'error')
↓
ExceptionView
→ on local (config('live') === 'false'): dump() the error details
→ on production: generic message
DGZ_Controller::display() additionally wraps controller actions in its own try/catch: an uncaught DGZ_Exception (or any \Throwable) is rendered through ExceptionView, with a last-resort fallback if the view itself fails.
DGZ_Exception
All framework errors are thrown as DGZ_Exception (core/DGZ_Exception.php). It extends \Exception and adds a typed error constant and an optional developer hint.
throw new DGZ_Exception(
'User not found',
DGZ_Exception::NO_USER_RECORD,
'Check that the user ID exists before calling this method.'
);
The constructor signature is:
public function __construct($message, $errorType = self::EXCEPTION, $hint = 'No hint available')
Exception type constants
| Constant | Value |
|---|---|
EXCEPTION |
exception |
NO_MENU_DEFINED |
noMenuDefined |
NO_LAYOUT_FOUND |
noLayoutFound |
NO_VIEW_FOUND |
noViewFound |
FIELD_NOT_FOUND_ON_TABLE |
fieldNotFoundOnTable |
FILE_NOT_FOUND |
fileNotFound |
CLASS_NOT_FOUND |
classNotFound |
NO_VALIDATOR_FOUND |
noValidatorFound |
QUERY_ERROR |
queryError |
PERMISSION_DENIED |
permissionError |
MISSING_PARAMETERS |
missingParametersError |
IDENTIFIER_NOT_FOUND |
identifierNotFound |
WRONG_PARAMETER_TYPE |
wrongParameterType |
INCORRECT_USERNAME_PASSWORD |
incorrectUsernameOrPassword |
NO_USER_RECORD |
noUserRecord |
WRONG_ADAPTER_FOR_MODEL |
wrongAdapterForModel |
MISSING_HANDLER_FOR_ACTION |
missingHandlerForAction |
CONTROLLER_CLASS_NOT_FOUND |
controllerClassNotFound |
INVALID_INPUT |
invalidInput |
INVALID_CONFIG |
invalidConfig |
INVALID_PARAMETER_VALUE |
invalidParameterValue |
PHP_FATAL_ERROR / PHP_ERROR / PHP_WARNING / PHP_NOTICE / PHP_OTHER_ERROR |
PHP native errors converted by the error handler |
DATABASE_QUERY_ERROR |
databaseQueryError |
DATABASE_EXPECTED_RECORD_NOT_RETURNED |
databaseExpectedRecordNotReturned |
NOT_IMPLEMENTED_EXCEPTION |
notImplementedException |
NO_CONTEXT_PROVIDED |
noContextProvided |
GALLERY_ALBUM_NOT_FOUND |
galleryAlbumNotFoundException |
Access the type, hint and message on a caught exception:
try {
// ...
} catch (DGZ_Exception $e) {
$type = $e->getType(); // e.g. 'noUserRecord'
$hint = $e->getHint(); // developer hint string
$msg = $e->getMessage();
}
Production Error Alerts
When the app is live (config('live') === 'true'), the error handler in bootstrap/helpers.php automatically emails the site admin on every caught PHP error:
$messenger = new DGZ_Messenger();
$messenger->sendErrorLogMsgToAdmin($message);
Ensure SMTP is correctly configured in production so these alerts are delivered. See Email.
Validation Errors
Form and API validation failures use a separate exception class, Dorguzen\Core\Exceptions\ValidationException:
use Dorguzen\Core\Exceptions\ValidationException;
throw new ValidationException(
errors: $validatorErrors,
input: $_POST,
validationErrorMessages: $messages,
redirectTo: '/contact'
);
The constructor accepts $errors, $input, $validationErrorMessages, $message, $errorCode and $redirectTo (all optional). DGZ_Router catches ValidationException, builds a combined error message from getValidationErrorMessages(), flashes it via addErrors(), and redirects. You generally never throw this manually — the validation layer does it for you.
See Validation and Forms for the full flow.
Custom Exception Handling
To handle a specific exception type in a controller, catch it directly and inspect its type:
public function show($id): void
{
try {
$post = $this->blogService->getPost($id);
} catch (DGZ_Exception $e) {
if ($e->getType() === DGZ_Exception::DATABASE_EXPECTED_RECORD_NOT_RETURNED) {
$this->addErrors('Post not found.');
$this->redirect('blog');
return;
}
throw $e; // re-throw anything you don't handle
}
$this->display('blog/show', compact('post'));
}
Uncaught exceptions bubble up to DGZ_Controller::display(), which logs them and renders ExceptionView.
The awesome dgzie() helper function
The DGZ framework has a great debugging helper function dgzie(), which is a wrapper to Symfony's VarDumper(). Here are some notes about it:
- It is a wrapper around Symfony's VarDumper
- It's defined in
bootstrap/helpers.php - To use it, just pass it a comma-separated list of things you want to display and halt execution of PHP. This list can be made of variables, literal items, objects, arrays etc. For example:
dgzie($sql, $params);
-
The output spots very useful information about the code being viewed, like:
- the line the
dgzie()function is called on - the current request route
- the state of the current request object
- and of course, the contents of the items you passed in to be dumped to the screen.
- As a bonus, it also has good color-coding, and drop-down arrows to reveal nested object and array elements so as to fit nicely on your screen.
- the line the
Writing custom logs for your application
Beside PHP's built-in error logs which Dorguzen does well to catch and handle elegantly, there are always times when you need to debug a certain feature or piece of code and need to be able to log data to efficiently reverse-engineer and pin-point what is going on. For that you can always use the handy PHP error_log() global function anywhere in your code. This will write logs to the php_error.log file of your PHP installation. This file on a MAMP installation on a Mac is located here:
/Applications/MAMP/logs/php_error.log
But you may not always have access to this log file, especially if you are working on a shared remote hosting server. For that you have two options:
- Tell the built-in
error_log()function to write to a specific log file of your choice. Just pass it the target file location in the third argument (see syntax below). - or DGZ has its own logging system that you can use. It's provided by the class
Dorguzen\Core\DGZ_Logger.php.
To use PHP's error_log() to write logs to a custom file, you need to use the 3rd argument. Yes, it has 4 arguments, and the following is an explanation of the 4 arguments:
error_log(
string message,
int message_type = 0,
?string destination = null,
?string extra_headers = null
): bool
message_type can be either 0, 1, 2, 3, or 4:
| Value | Meaning |
|---|---|
| 0 | Send to PHP system logger (default error log) |
| 1 | Send email (requires extra_headers) |
| 2 | Send to debug connection (not commonly used) |
| 3 | ✅ Write message to a file specified in the third argument - the one you need ... |
| 4 | Send directly to SAPI logging handler |
So, when you write:
error_log("DGZ_Request object requested", 3, __DIR__ . '/../storage/logs/dgz_errors.log');
You are saying:
3 → write to a custom file
third argument → the file path to write to
This is the correct way to log to a specific file using error_log().
Creating custom logs with DGZ_Logger
Here is how custom logs in Dorguzen work, in a nutshell. There are three parts:
- configuration
Dorguzen\Core\DGZ_Loggermodels\Logs
First, you have to define the type of logging you want in the Dorguzen\Configs\Config.php file. It basically determines where you prefer logs to be written to. The setting is defined in the
'log_driver'
key. The value can be one of three options:
'file'
'db'
'both'
But the default value is 'db'. You can always find logs for your application in the logs table, unless you chose 'file' as the driver.
You can also specify the format using the
'log_format'
key. The value can be one of two options:
'text'
'json'
But the default is 'text'.
Essentially, you would call the static method log() of DGZ_Logger and pass it the actual string to be logged. This log() method uses the Logs model to save the logs data to the database if your configuration specifies that.
Here is the full documentation on how to write custom logs in Dorguzen.
✅ Dorguzen Logging System — Full Documentation
The Dorguzen Logging System provides a powerful, configurable, and developer-friendly way to record runtime information from your application. It is designed to work both in local development and in restricted shared hosting environments where you cannot access PHP error logs (e.g., GoDaddy Shared Hosting).
The logger is implemented through three main classes:
✅ 1. Core Components (Classes Involved)
a) Dorguzen\Configs\Config
This class provides global framework configuration, including the logging settings:
- Logging driver (file, db, or both)
- Logging format (text or json)
- Log directory path (defaults to
storage/logs)
b) Dorguzen\Core\DGZ_Logger
This is the core logging engine. It:
- Accepts log messages at multiple severity levels (debug, info, warning, error, critical)
- Handles file creation, rotation, and concurrency-safe writes
- Forwards logs to the database via the Logs model
- Supports text and JSON structured logging
- Automatically creates missing directories
- Uses PSR-3-style method signatures (but simplified)
c) src\models\Logs
This is the ORM model responsible for database logging. It now also stores context data in a context_json field (JSON encoded).
This allows you to build structured, filterable logs directly inside your application.
✅ 2. Logging Drivers
The behavior of the logger is controlled by:
'log_driver' => 'db', // file | db | both
'log_format' => 'text', // text | json
These values live inside configs/Config.php.
Driver Options
| Driver | Description |
|---|---|
| file | Writes logs to rotating files under /storage/logs/. |
| db | Logs messages into your logs database table. |
| both | Writes to file AND database simultaneously. |
✅ 3. Logging Formats
You may choose how each line is written when using the file driver.
'log_format' => 'text', // default
// or:
'log_format' => 'json'
The text Output Example
[2025-11-10 15:04:21] ERROR: Payment failed
The json Output Example
{"timestamp":"2025-11-10 15:04:21","level":"ERROR","message":"Payment failed","context":{"txn":5533}}
JSON logs are machine-readable and excellent for future analytics.
✅ 4. How Logging Works (Chronological Explanation)
Below is the full flow of how a log message is processed from start to finish.
✅ Step 1 — Logger Initialization
In bootstrap/app.php, your framework initializes the logger:
DGZ_Logger::init(__DIR__ . '/../storage/logs/dgz_errors.log');
During initialization:
- The logger builds its internal settings (driver, format, etc.)
- Ensures the directory
/storage/logsexists - Ensures the directory is writable
- Creates a Logs model instance for DB logging
- Prepares to create today's log file
✅ Step 2 — Daily Log File Determination
Each day creates its own file:
/storage/logs/dgz-2025-11-10.log
This is called daily log rotation, and it prevents massive single log files from forming.
✅ Step 3 — Writing Logs
When you call:
DGZ_Logger::error("User login failed", ['email' => 'test@example.com']);
The logger:
- Normalizes the severity level
- Formats the message depending on text/json mode
- Writes to file, using
flock()for concurrency-safe writes - Writes to DB via
$logsModel->log($level, $message, $context);
Thanks to the new context_json column, the DB now stores structured context data.
✅ 5. Cool Features of the Logging system
The Dorguzen logging system is equivalent in spirit to a lightweight version of Laravel's Monolog integration, but written natively with zero external dependencies.
Here are its strongest features:
a) Safe Concurrent Writes (flock)
Multiple requests writing at once will not corrupt the log file.
b) Daily Log Rotation
Automatically creates a new file per day: dgz-YYYY-MM-DD.log
c) Dual Drivers (file, db, both)
You can:
- log only to file
- log only to DB
or both at the same time
Right from your configuration.
d) Optional JSON Structured Logging
This is perfect for servers, analytics tools, and future machine learning integration.
e) Log Levels
debug, info, warning, error, critical
Same naming conventions used by PSR-3 and Laravel.
f) PSR-3-Style Method Signatures
The API feels familiar for anyone used to modern logging libraries:
DGZ_Logger::error($msg, $context);
g) Integrates Cleanly With Your Logs Model
All context is stored into context_json, providing:
- filterable logs
- human readable output
- selectable context keys
- advanced debugging
h) Fully Extensible
You can easily attach:
- Email notifications
- Slack / Discord alerts
- Webhooks
- Custom rotating strategies
Because the logger architecture is very clean.
✅ 6. Example Usage
Basic Log
DGZ_Logger::info("User logged in");
✅ Log with Context
DGZ_Logger::warning("Slow DB query", [
'duration_ms' => 240,
'sql' => 'SELECT * FROM users'
]);
✅ Critical Error
DGZ_Logger::critical("Payment gateway unavailable", [
'gateway' => 'Stripe'
]);
✅ 7. Example Output
File (text mode)
[2025-11-10 15:32:10] INFO: User logged in
[2025-11-10 15:32:12] WARNING: Slow DB query | {"duration_ms":240,"sql":"SELECT * FROM users"}
[2025-11-10 15:32:20] CRITICAL: Payment gateway unavailable | {"gateway":"Stripe"}
File (json mode)
{"timestamp":"2025-11-10 15:32:20","level":"CRITICAL","message":"Payment gateway unavailable","context":{"gateway":"Stripe"}}
Database Row Example
| id | title | message | context_json | logs_created |
|---|---|---|---|---|
| 1 | CRITICAL | Payment gateway unavailable | {"gateway":"Stripe"} | 2025-11-10 |
✅ 8. What This Unlocks (The Future)
Because this system is structured, stable, and extensible, you can now build:
- Searchable, filterable admin UI
- Filter logs by:
- level
- date
- keywords
- context keys
- Trend analytics & monitoring dashboards
- Plot errors by day, by controller, by model, etc.
- Machine-learning / anomaly detection
JSON logs make it possible.
- Rich developer experience
Even on shared hosting with no access to error_log.
- Automated alert pipelines
Plug-in logic can email you on CRITICAL failures.
Final Thoughts
The new Dorguzen Logging System is:
- ✅ Console-framework-grade
- ✅ Concurrency safe
- ✅ Extremely configurable
- ✅ PSR-3-aligned
- ✅ Extends neatly without rewriting anything
- ✅ Equivalent in spirit to a small Monolog engine
- ✅ Designed for real-world hosting constraints
You may want to clear your logs, sometimes, otherwise it will keep logging for ever. There you have it; one of the strongest and most flexible subsystems in Dorguzen — a very useful tool for debugging. Happy logging.
Log Channels
So far the examples above all use DGZ_Logger::error() / info() / etc., which write to a single, shared destination controlled by APP_LOG_DRIVER and APP_LOG_FORMAT in your .env file.
A log channel is a named, independently-configured logging destination. Instead of one global stream you can have several — one per concern:
payments → file-only, JSON, warnings and above
security → file + DB, JSON, errors and above
default → whatever APP_LOG_DRIVER says (backwards-compatible with all existing DGZ_Logger calls)
Every channel has its own:
driver — where to write: 'file' | 'db' | 'both'
format — line format: 'text' | 'json'
path — directory for log files, relative to the project root
min_level — minimum severity to record: debug | info | notice | warning | error | critical
Messages below the threshold are silently discarded.
Configuring channels
Open (or create) configs/logging.php. The ConfigLoader picks it up automatically — no registration needed.
<?php
return [
'channels' => [
// 'default' mirrors your .env APP_LOG_DRIVER / APP_LOG_FORMAT settings
// so all existing DGZ_Logger::error() calls are completely unaffected.
'default' => [
'driver' => env('APP_LOG_DRIVER', 'db'),
'format' => env('APP_LOG_FORMAT', 'text'),
'path' => 'storage/logs',
'min_level' => 'debug',
'filename_prefix' => 'dgz', // produces dgz-YYYY-MM-DD.log
],
'payments' => [
'driver' => 'file', // file only — never touches the DB
'format' => 'json',
'path' => 'storage/logs',
'min_level' => 'warning',
],
'security' => [
'driver' => 'both', // file AND DB
'format' => 'json',
'path' => 'storage/logs',
'min_level' => 'error',
],
],
];
Using channels in your application code
Anywhere you have access to DGZ_Logger you can call ::channel():
DGZ_Logger::channel('payments')->warning('Charge failed', ['amount' => 5000, 'user' => $userId]);
DGZ_Logger::channel('security')->error('Login brute force', ['ip' => $ip, 'attempts' => 12]);
DGZ_Logger::channel('security')->critical('Privilege escalation attempt', ['user' => $user]);
The API is identical to the top-level DGZ_Logger convenience methods (debug/info/notice/warning/error/critical), so switching between global and channel-based logging is a one-word change.
Channel instances are cached per request — calling ::channel('payments') ten times only creates one object.
File naming
By default a channel writes to:
{path}/{channelName}-YYYY-MM-DD.log e.g. payments-2025-11-10.log
You can override the filename prefix with the optional filename_prefix key in the channel config:
'filename_prefix' => 'dgz' → dgz-2025-11-10.log
The 'default' channel uses filename_prefix: 'dgz' so its files match the legacy format produced before channels were introduced.
Filtering DB logs by channel
When driver is 'db' or 'both', the channel name is injected into the context_json column:
{"_channel": "security", "ip": "1.2.3.4", "attempts": 12}
This lets you query or filter by channel in phpMyAdmin, your admin panel, or a future log viewer:
SELECT * FROM logs WHERE context_json LIKE '%"_channel":"security"%';
Viewing logs via the CLI (php dgz log)
The php dgz log command displays recent log entries directly in your terminal:
php dgz log
Important: this command reads exclusively from the DB (the logs table). It does not read any log files. This means:
- Channels with driver 'db' or 'both' — their entries appear in the output.
- Channels with driver 'file' — their entries do NOT appear; they only exist on disk.
To make the blind spot visible, the command automatically prints a notice at the bottom of its output whenever it detects one or more file-only channels in your config:
Note: channel(s) [payments] use driver 'file' — their entries are not stored in the DB
and do not appear above.
To inspect: php dgz log:tail --channel=payments
This notice is purely informational — it does not affect the exit code or the log output above it.
If you want a channel's entries to appear in php dgz log, change its driver to 'db' or 'both' in configs/logging.php. The trade-off is that 'both' writes every entry twice (file + DB row), which adds a small write overhead but gives you both the raw file archive and the structured DB view simultaneously.
Log Tailing
Log tailing lets you watch a log file update in real time in your terminal — identical to running tail -f manually, but integrated into the Dorguzen CLI so you never have to remember the file path.
The command
Tail the default channel (driver must be 'file' or 'both'):
php dgz log:tail
Tail a named channel:
php dgz log:tail --channel=payments
php dgz log:tail -c payments
Show 50 lines of history before following new output (default is 20):
php dgz log:tail --channel=security --lines=50
php dgz log:tail -c security -l 50
The command streams output to your terminal and keeps running until you press Ctrl+C. It tells you which file it is watching so there is never any ambiguity.
Notes:
- Requires the channel's driver to be 'file' or 'both'. A db-only channel has no file.
- The log file is created on the first write. If none exists yet, the command tells you what to do.
tailmust be available on the host OS. It is standard on macOS and every Linux distribution.
How to test it — a complete working example
This is the fastest way to see channels and tailing working end-to-end. Open two terminal tabs side-by-side.
Step 1 — make sure the payments channel writes to a file. In configs/logging.php confirm (or set):
'payments' => [
'driver' => 'file',
'format' => 'json',
'path' => 'storage/logs',
'min_level' => 'warning',
],
Step 2 — in Terminal A, start tailing:
php dgz log:tail --channel=payments
You will see:
Tailing [payments]: /path/to/project/storage/logs/payments-2025-11-10.log
Press Ctrl+C to stop.
If the file does not exist yet the command will print a hint — just continue to Step 3.
Step 3 — in Terminal B, write a log entry. The quickest way is a tiny PHP one-liner from the project root:
php -r "
require_once __DIR__ . '/tests/manual/cliTestHeader.php';
\Dorguzen\Core\DGZ_Logger::channel('payments')->warning('Test charge failed', ['amount' => 100]);
"
Or, add this temporarily to any controller action and visit that page in your browser:
DGZ_Logger::channel('payments')->warning('Test charge failed', ['amount' => 100]);
DGZ_Logger::channel('payments')->error('Payment gateway timeout', ['gateway' => 'Stripe']);
Step 4 — watch Terminal A. Each log line appears in Terminal A the moment it is written:
{"time":"2025-11-10 14:32:01","channel":"payments","level":"WARNING","message":"Test charge failed","context":{"amount":100}}
{"time":"2025-11-10 14:32:01","channel":"payments","level":"ERROR","message":"Payment gateway timeout","context":{"gateway":"Stripe"}}
Press Ctrl+C in Terminal A to stop tailing.
That is it. From here you can add more channels to configs/logging.php for any concern in your application — audit trails, slow query logs, third-party API calls — each independently configured and independently tailored.
Lifespan of log files
You may be wondering, if we only ever see the file of the current date, what's the use of log files of other dates. What are its use cases?
Here is the thing. The daily log files are primarily for retrospective investigation, not live monitoring. Here are the real-world use cases:
1. Post-mortem debugging
Something broke at 2am on Tuesday. You wake up Wednesday, see reports, and need to know exactly what happened. You open payments-2026-02-24.log and trace the exact sequence of events — timestamps, context data, everything.
2. Incident timeline reconstruction
A user reports their payment failed 'sometime last week.' Without the old log files you'd have no evidence. With them you can pull up the exact date range and search:
grep 'user_id.*4521' storage/logs/payments-2026-02-18.log
3. Spotting patterns across time
Errors that happen every Monday at 9am (scheduled job clash), or every month-end (heavy load), or only on specific days. You'd never notice this from today's file alone.
4. Auditing and compliance
Financial transactions, auth events, and security logs often need to be retained for legal/compliance reasons (30 days, 90 days, 1 year depending on your industry). Camerooncom handles payments, so this is relevant.
5. Comparing before/after a deployment
You shipped code on Friday. Something seems off now. You compare Saturday's log against Thursday's to see what changed in behaviour.
So the question becomes: how long should you keep them?
The common approach is log rotation with a retention policy — keep N days, then auto-delete. Tools like logrotate (Linux) handle this, or you could add a log:prune --days=30 command to the CLI. Most production apps keep 30-90 days of file logs and rely on the DB for longer-term structured querying.
Since Dorguzen provides you with a DB driver for logs as well, old file logs are mostly a safety net and a fast grep-able archive. The DB gives you the structured, queryable view; the files give you the raw, never-filtered record.
Log Pruning
Log pruning lets you enforce a file retention policy from the CLI — deleting log files older than a chosen number of days — without touching anything by hand. A DB audit record is always written after every run (including dry-runs) so there is a permanent, queryable history of what was pruned and when.
The command
Prune all channels, delete files older than 30 days (the default):
php dgz log:prune
Use a custom cutoff:
php dgz log:prune --days=7
php dgz log:prune --days=90
Limit to one channel:
php dgz log:prune --channel=payments
php dgz log:prune -c payments
Combine options:
php dgz log:prune --channel=security --days=90
Preview what would be deleted without touching anything (--dry-run):
php dgz log:prune --dry-run
php dgz log:prune --channel=payments --days=7 --dry-run
List every log file regardless of age (days=0 means "everything is old"):
php dgz log:prune --days=0 --dry-run
Options summary
--days, -d (default: 30)
Files whose last-modified time is more than this many days ago are candidates for deletion. Use 0 to target every log file.
--channel, -c (default: all)
Restrict pruning to a single named channel. The name must match a key in configs/logging.php. Channels with driver 'db' are automatically skipped (they have no files). Pass 'all' (or omit the option) to process every file-backed channel.
--dry-run (flag, no value)
Report which files would be deleted without actually deleting them. The DB audit entry is still written so you have a record of the intent.
How channel ownership is determined
The command identifies which files belong to a channel by filename prefix, not by subdirectory. Multiple channels can share the same directory (e.g. storage/logs/) and the command will still only touch the files that belong to each channel.
The prefix follows the same rule used by the logger when creating files:
- If the channel config has a
filename_prefixkey, that value is used. - Otherwise the channel name itself is the prefix.
So for the default configs/logging.php setup:
Channel 'default' → prefix 'dgz' → matches dgz-YYYY-MM-DD.log
Channel 'payments' → prefix 'payments' → matches payments-YYYY-MM-DD.log
Channel 'security' → prefix 'security' → matches security-YYYY-MM-DD.log
Each channel only sees its own files. A file like payments-2026-02-25.log will never be counted or deleted when pruning the 'default' or 'security' channel.
If you ever configure a channel to use a dedicated subdirectory (e.g. path: storage/logs/payments), both the prefix filter and the directory are scoped per channel, so isolation is maintained either way.
The --dry-run safety net
Always run with --dry-run first, especially when setting a new retention window. The output shows exactly which files are in scope, their age in days, and the final count:
[dry-run] No files will be deleted.
Channel 'default' — scanning /path/to/project/storage/logs
[would delete] dgz-2025-11-10.log (age: 106d)
[would delete] dgz-2025-11-11.log (age: 105d)
Dry run complete. 2 file(s) would be deleted, 3 kept.
Once you are happy with what you see, run the same command without --dry-run to commit the deletion.
DB audit record
After every run (real or dry), the command writes one row to the logs table via Logs::log(). This bypasses the channel driver setting so the audit ALWAYS reaches the DB, even if you disabled DB logging for a channel.
The row looks like:
logs_title: INFO
logs_message: log:prune — 2 file(s) deleted
context_json: {"days":30,"channel":"all","deleted":2,"skipped":3,"dry_run":false}
To query recent prune history:
SELECT * FROM logs WHERE logs_title = 'INFO'
AND logs_message LIKE 'log:prune%'
ORDER BY logs_created DESC;
Or via the CLI:
php dgz log
Recommended retention strategy
File logs — keep for 30 days by default.
php dgz log:prune --days=30
High-sensitivity channels (security, payments) — keep longer for compliance.
php dgz log:prune --channel=security --days=90
php dgz log:prune --channel=payments --days=90
DB logs — keep indefinitely. They are compact (no raw text, just structured rows) and are the primary source for dashboards and audit queries.
You can automate pruning with a cron job (Linux/macOS):
# Run every day at 2am, prune files older than 30 days
0 2 * * * cd /path/to/project && php dgz log:prune --days=30 >> /dev/null 2>&1
Graceful edge cases
- Unknown channel — the command prints an error and exits with a non-zero code.
php dgz log:prune --channel=unknown
→ [error] Unknown channel 'unknown'. Check configs/logging.php for valid channel names.
- Channel with driver 'db' — skipped silently with an informational note.
- Log directory does not exist — skipped with a warning; the command continues with other channels.
- File permission error — reported per-file; the run continues and the DB audit counts it as skipped.
- No DB connection — the audit write failure is caught and reported as a warning; the exit code is still SUCCESS since the file operations succeeded.
The difference between PHP's error_log() and DGZ_Logger
You can use either, PHP's error_log() or Dorguzen's DGZ_Logger's log() function to write custom logs, but here's a comparison:
error_log() advantages
Here are the advantages of using error_log() for logging:
- Handles file locking internally
- OS-level optimized
- Good for append-only logs
- Less likely to cause race conditions
file_put_contents() advantages
- Full control over format
- Can create directories first
- Can rotate logs more easily
For writing logs to a custom file, either is fine — but DGZ's DGZ_Logger::log() works with your config settings to manage how you log data — via DB, file, or both. Having the logs in your DB can mean that you can display them in log report screens for other members of your team to analyse.
How to write values to console during PHPUnit tests
Do a try {} and catch block, and in the catch block, throw a RuntimeException exception like this:
throw new \RuntimeException(
'Config WAS loaded successfully in bootstrap/testing.php Config dump: ' .
json_encode($this->config->getConfig())
)