timefrontiers / php-has-errors
Standardized error handling trait for PHP classes
Requires
- php: >=8.5
- timefrontiers/php-core: ^1.1
Requires (Dev)
- php-parallel-lint/php-parallel-lint: ^1.4
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.5
- timefrontiers/php-instance-error: ^1.1.1
Suggests
- timefrontiers/php-instance-error: For extracting and filtering errors by user rank
This package is auto-updated.
Last update: 2026-08-20 18:58:32 UTC
README
Canonical, context-grouped error collection for PHP 8.5 and later. The package
provides the HasErrors trait and a small public provider contract while
keeping technical diagnostics separate from deliberately public messages.
Installation
composer require timefrontiers/php-has-errors:^1.1
Version 1.1 requires PHP 8.5 or later and timefrontiers/php-core:^1.1.
timefrontiers/php-instance-error:^1.1.1 remains optional and provides
rank-aware extraction and logging for consumers.
Canonical contract
Errors are grouped by a non-empty operation context. Every entry is exactly one five-element list:
/** @var array{0: int, 1: int, 2: string, 3: string, 4: int} $error */ $error = [$minimumRank, $code, $message, $file, $line]; /** @var array<string, list<array{0: int, 1: int, 2: string, 3: string, 4: int}>> $errors */ $errors = ['save' => [$error]];
| Index | Meaning | Contract |
|---|---|---|
0 |
Minimum access rank | Integer from AccessRank::GUEST (0) through AccessRank::OWNER (14) |
1 |
PHP error code | Positive integer; helpers default to E_USER_ERROR |
2 |
Message | Non-empty string |
3 |
Source file | Optional internal string; path separators are normalized to / |
4 |
Source line | Non-negative integer; 0 means unavailable |
File and line are trusted diagnostic metadata. Never include them in a guest or user response.
Provider contract
Make a provider explicit with TimeFrontiers\Contract\ErrorProvider. A class
using the trait already has the required getErrors(): array method, but PHP
requires the consuming class to declare the interface itself:
use TimeFrontiers\Contract\ErrorProvider; use TimeFrontiers\Helper\HasErrors; final class ImportService implements ErrorProvider { use HasErrors; public function import(array $records): bool { if ($records === []) { $this->_userError('import', 'Select at least one record.'); return false; } return true; } }
A callable public getErrors() method remains supported for 1.0 compatibility
and is authoritative. If it throws or returns a non-array value, merging is a
no-op; the extractor will not fall back to a property and accidentally bypass
the provider contract.
Adding errors
use TimeFrontiers\AccessRank; $this->_addError( context: 'save', message: 'Persistence operation failed; see the protected log reference.', min_rank: AccessRank::DEVELOPER->value, code: E_USER_ERROR, );
The protected convenience methods assign these ranks:
| Method | Rank | Intended content |
|---|---|---|
_userError() |
Guest (0) |
Deliberately safe, actionable public text |
_internalError() |
Moderator (4) |
Staff process diagnostics |
_systemError() |
Developer (7) |
Redacted technical diagnostics |
_debugError() |
Superadmin (8) |
Restricted debug diagnostics |
Invalid local configuration—blank context/message, a rank outside 0..14, a
non-positive code, or a negative line—throws InvalidArgumentException.
Public messages versus diagnostics
Rank filtering is not a data-safety classification. A rank-zero tuple imported from a legacy source might still contain SQL, credentials, tokens, personal data, a raw provider failure, or another technical detail.
Use the dedicated projection at a public response boundary:
$messages = $service->publicErrorMessages('import'); $first = $service->firstPublicError('import');
These methods return only messages deliberately registered on that same object
through _userError(). Generic _addError() entries and imported tuples are
excluded even when their minimum rank is Guest. firstError() and
errorMessages() remain diagnostic/rank-filtering compatibility APIs and do
not establish that a message is safe for public output.
The package never sanitizes diagnostic content or writes logs. Send sensitive technical detail directly to an access-controlled, redacting logger; keep only a safe public message and an opaque internal correlation reference here.
Merging errors
// One source context, retaining its name. $this->_mergeErrors($repository, 'save'); // One source context renamed for the receiver. $this->_mergeErrors($repository, 'save', 'create'); // All source contexts flattened in deterministic order into "merged". $this->_mergeErrors($repository); // All source contexts flattened into an explicit target. $this->_mergeErrors($repository, '', 'create');
Unscoped InstanceError::get() correctly remains grouped by context. Version
1.1 normalizes that grouped output and imports individual tuples, fixing the
1.0 empty-context corruption without changing InstanceError's contract.
Every imported collection and tuple is validated. Malformed contexts, nested lists, invalid ranks/codes, and invalid message/file/line types are rejected. Unsupported sources, inaccessible properties, throwing providers, and malformed providers are non-fatal no-ops. Repeating an identical merge does not append historical duplicates; newly observed canonical tuples are appended in source order.
The optional InstanceError adapter is used when available. Without it, the
fallback can read only exact public legacy $errors or $_errors arrays.
Protected and private members are never probed or read.
Reading and clearing
$all = $service->getErrors(); $hasAny = $service->hasErrors(); $hasSave = $service->hasErrors('save'); $count = $service->errorCount('save'); $firstDiagnostic = $service->firstError('save'); $guestRankedDiagnostics = $service->errorMessages( 'save', AccessRank::GUEST->value, ); $service->clearErrors('save'); $service->clearErrors();
A null rank on firstError() or errorMessages() returns trusted diagnostic
messages without filtering. Explicit ranks must be inside the AccessRank range.
Clearing errors also clears the corresponding public-message projection.
Upgrading from 1.0
- Remove any package-level Composer
version; tags determine the version. - Require PHP 8.5 and Core 1.1.
- Empty-context merges now produce canonical tuples rather than nested lists.
- Callable public providers are authoritative; inaccessible members never trigger direct-property access.
- Imported data is strictly normalized and repeated identical merges are idempotent.
- Use
publicErrorMessages()/firstPublicError()for public response text. - Do not copy raw SQL, gateway failures, credentials, tokens, personal data, file paths, or source lines into standard public messages.
Development
composer validate --strict --no-check-publish composer update composer audit --locked composer check
The repository does not commit composer.lock. CI resolves both highest and
lowest supported dependencies on PHP 8.5.
License
MIT