Standardized error handling trait for PHP classes with rank-based visibility filtering.
composer require timefrontiers/php-has-errors- PHP 8.1+
- (Optional)
timefrontiers/php-instance-errorfor advanced error extraction
use TimeFrontiers\Helper\HasErrors;
class PaymentService {
use HasErrors;
public function charge(float $amount):bool {
if ($amount <= 0) {
$this->_userError('charge', 'Amount must be positive');
return false;
}
if (!$this->gateway->process($amount)) {
$this->_systemError('charge', 'Gateway returned: ' . $this->gateway->lastError());
$this->_userError('charge', 'Payment could not be processed. Please try again.');
return false;
}
return true;
}
}
// Usage
$payment = new PaymentService();
if (!$payment->charge(100.00)) {
// Guest sees: "Payment could not be processed. Please try again."
// Developer sees: "Gateway returned: Connection timeout" + user message
$errors = (new InstanceError($payment))->get('charge');
}Errors are stored as arrays with 5 elements:
[$min_rank, $code, $message, $file, $line]| Index | Name | Type | Description |
|---|---|---|---|
| 0 | min_rank |
int |
Minimum AccessRank to view this error |
| 1 | code |
int |
PHP error code (e.g., 256 = E_USER_ERROR) |
| 2 | message |
string |
Human-readable error message |
| 3 | file |
string |
Source file (auto-captured) |
| 4 | line |
int |
Line number (auto-captured) |
use TimeFrontiers\AccessRank;
AccessRank::GUEST; // 0 - Public users
AccessRank::USER; // 1 - Logged in users
AccessRank::ANALYST; // 2
AccessRank::ADVERTISER; // 3
AccessRank::MODERATOR; // 4 - Staff (can see internal errors)
AccessRank::EDITOR; // 5
AccessRank::ADMIN; // 6
AccessRank::DEVELOPER; // 7 - Technical (can see system errors)
AccessRank::SUPERADMIN; // 8 - Can see debug errors
AccessRank::OWNER; // 14 - Full accessAdd an error with full control:
$this->_addError(
context: 'save',
message: 'Database connection failed',
min_rank: AccessRank::DEVELOPER->value,
code: 256
);
// File and line are auto-captured from callerMerge errors from another instance:
if (!$this->repository->save($entity)) {
$this->_mergeErrors($this->repository, 'save', 'create');
return false;
}| Method | Visible To | Use For |
|---|---|---|
_userError() |
Everyone (rank 0+) | Validation, user input, friendly messages |
_internalError() |
Staff (rank 4+) | Business logic, process failures |
_systemError() |
Developers (rank 7+) | SQL errors, connections, system issues |
_debugError() |
Superadmin (rank 8+) | Sensitive debug info |
// User sees this
$this->_userError('login', 'Invalid email or password');
// Only developers see this
$this->_systemError('login', "SQL: {$sql}\nError: {$db_error}");// Get all errors
$errors = $instance->getErrors();
// Check for errors
if ($instance->hasErrors('save')) { ... }
if ($instance->hasErrors()) { ... } // Any context
// Count errors
$count = $instance->errorCount('save');
$total = $instance->errorCount();
// Clear errors
$instance->clearErrors('save'); // Specific context
$instance->clearErrors(); // All
// Get first error message
$message = $instance->firstError('save');
$message = $instance->firstError('save', AccessRank::GUEST->value);
// Get all messages
$messages = $instance->errorMessages('save');
$messages = $instance->errorMessages('save', AccessRank::GUEST->value);use TimeFrontiers\InstanceError;
$service = new MyService();
if (!$service->process()) {
// Get errors filtered by current user's rank
$errors = (new InstanceError($service))->get('process');
// Get all errors (bypass rank filter)
$all = (new InstanceError($service, true))->get('process');
// Get only messages
$messages = (new InstanceError($service))->get('process', true);
}use TimeFrontiers\Helper\HasErrors;
use TimeFrontiers\AccessRank;
class OrderService {
use HasErrors;
public function __construct(
private OrderRepository $orders,
private PaymentGateway $payment
) {}
public function placeOrder(array $data):Order|false {
// Validate
if (empty($data['items'])) {
$this->_userError('placeOrder', 'Cart is empty');
return false;
}
// Create order
$order = new Order($data);
if (!$this->orders->save($order)) {
$this->_mergeErrors($this->orders, 'save', 'placeOrder');
$this->_userError('placeOrder', 'Unable to create order. Please try again.');
return false;
}
// Process payment
if (!$this->payment->charge($order->total)) {
$this->_systemError('placeOrder', 'Payment failed: ' . $this->payment->lastError());
$this->_userError('placeOrder', 'Payment declined. Please check your card details.');
// Rollback
$this->orders->delete($order);
return false;
}
return $order;
}
}
// Controller usage
$service = new OrderService($orders, $payment);
if ($order = $service->placeOrder($request->all())) {
return Response::success('Order placed', $order);
}
// Get user-appropriate errors
$errors = (new InstanceError($service))->get('placeOrder', true);
return Response::error('Order failed', $errors);Before:
class MyClass {
public $errors = [];
public function doSomething():bool {
if ($failed) {
$this->errors['doSomething'][] = [0, 256, 'Failed', __FILE__, __LINE__];
return false;
}
return true;
}
}After:
class MyClass {
use HasErrors;
public function doSomething():bool {
if ($failed) {
$this->_userError('doSomething', 'Failed');
return false;
}
return true;
}
}MIT License