timefrontiers/php-has-errors

Standardized error handling trait for PHP classes

Maintainers

Package info

github.com/timefrontiers/php-has-errors

pkg:composer/timefrontiers/php-has-errors

Transparency log

Statistics

Installs: 77

Dependents: 8

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-08-20 18:58 UTC

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