PHPackages                             phpdot/error - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Validation &amp; Sanitization](/categories/validation)
4. /
5. phpdot/error

ActiveLibrary[Validation &amp; Sanitization](/categories/validation)

phpdot/error
============

Structured error codes with context, translatable messages, and uniform output across every channel.

v1.2.1(4d ago)0111MITPHPPHP &gt;=8.4

Since Apr 5Pushed 2mo agoCompare

[ Source](https://github.com/phpdot/error)[ Packagist](https://packagist.org/packages/phpdot/error)[ RSS](/packages/phpdot-error/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (16)Versions (6)Used By (1)

phpdot/error
============

[](#phpdoterror)

Structured error codes with context, translatable messages, and uniform output across every channel. Optional translator integration via `phpdot/contracts`.

---

Table of Contents
-----------------

[](#table-of-contents)

- [Install](#install)
- [Quick Start](#quick-start)
- [Architecture](#architecture)
    - [Flow](#flow)
    - [Package Structure](#package-structure)
- [Defining Error Codes](#defining-error-codes)
    - [ErrorCodeInterface](#errorcodeinterface)
    - [ErrorCodeTrait](#errorcodetrait)
    - [Module Error Enums](#module-error-enums)
    - [Error Code Convention](#error-code-convention)
- [ErrorEntry (The DTO)](#errorentry-the-dto)
- [ErrorBag (The Collector)](#errorbag-the-collector)
    - [Adding Errors](#adding-errors)
    - [Checking Errors](#checking-errors)
    - [Filtering Errors](#filtering-errors)
    - [Merging Bags](#merging-bags)
    - [HTTP Status](#http-status)
    - [Serialization](#serialization)
- [ErrorType (9 Categories)](#errortype-9-categories)
- [HttpStatus (Typed Enum)](#httpstatus-typed-enum)
- [Context — What the Error Relates To](#context)
- [Translation (i18n)](#translation-i18n)
    - [How It Works](#how-translation-works)
    - [ICU Params](#icu-params)
    - [Frontend Translation](#frontend-translation)
    - [Server-Side Translation](#server-side-translation)
- [Output Formats](#output-formats)
    - [JSON API](#json-api)
    - [HTML Forms](#html-forms)
    - [CLI](#cli)
    - [WebSocket](#websocket)
- [Real-World Usage](#real-world-usage)
    - [Service Validation](#service-validation)
    - [Cross-Module Merge](#cross-module-merge)
    - [Frontend Grouping](#frontend-grouping)
- [API Reference](#api-reference)
    - [ErrorCodeInterface API](#errorcodeinterface-api)
    - [ErrorCodeTrait API](#errorcodetrait-api)
    - [ErrorEntry API](#errorentry-api)
    - [ErrorBag API](#errorbag-api)
    - [ErrorType API](#errortype-api)
    - [HttpStatus API](#httpstatus-api)
- [License](#license)

---

Install
-------

[](#install)

```
composer require phpdot/error
```

RequirementVersionPHP&gt;= 8.3phpdot/contracts^1.3---

Quick Start
-----------

[](#quick-start)

**1. Define errors for your module:**

```
enum UserErrors: string implements ErrorCodeInterface
{
    use ErrorCodeTrait;

    case NOT_FOUND     = '00010001';
    case EMAIL_TAKEN   = '00010002';
    case INVALID_EMAIL = '00010003';
    case WEAK_PASSWORD = '00010004';

    public function getDetails(): array
    {
        return match ($this) {
            self::NOT_FOUND => [
                'message'     => 'User not found',
                'description' => 'errors.user.not_found',
                'type'        => ErrorType::NOT_FOUND,
                'httpStatus'  => HttpStatus::NOT_FOUND->value,
            ],
            self::EMAIL_TAKEN => [
                'message'     => 'Email is already taken',
                'description' => 'errors.user.email_taken',
                'type'        => ErrorType::CONFLICT,
                'httpStatus'  => HttpStatus::CONFLICT->value,
            ],
            // ...
        };
    }
}
```

**2. Collect errors:**

```
$errors = new ErrorBag();           // raw mode — descriptions stay as keys
// or
$errors = new ErrorBag($translator); // descriptions are translated at add() time

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors->add(UserErrors::INVALID_EMAIL, 'email');
}

if (strlen($password) < 8) {
    $errors->add(UserErrors::WEAK_PASSWORD, 'password', ['min' => 8]);
}

if ($errors->hasErrors()) {
    return $errors; // same structure for JSON, HTML, WebSocket, CLI
}
```

**3. Same output everywhere:**

```
{
    "errors": [
        {
            "code": "00010003",
            "message": "Invalid email address",
            "description": "errors.user.invalid_email",
            "type": "validation",
            "httpStatus": 422,
            "context": "email",
            "params": []
        }
    ]
}
```

---

Architecture
------------

[](#architecture)

 ```
graph LR
    subgraph "Module side"
        ENUM[Module enumUserErrors, OrderErrors]
        ECI[ErrorCodeInterface]
        ECT[ErrorCodeTrait]
        ENUM -.->|implements| ECI
        ENUM -.->|uses| ECT
    end

    subgraph "Bag construction"
        FACTORY[ErrorBagFactory]
        BAG[ErrorBag]
        FACTORY -->|create| BAG
    end

    TRANS[MessageTranslatorInterface]
    FACTORY -->|optional| TRANS
    BAG -->|optional| TRANS

    ENUM -->|add into| BAG
    BAG -->|collects| ENTRY[ErrorEntrycode / description / type / httpStatus / context / params]

    BAG --> JSON["JSON APItoArray()"]
    BAG --> HTML["HTML formforContext()"]
    BAG --> CLI["CLIall()"]
    BAG --> WS["WebSockettoArray()"]

    style FACTORY fill:#2d3748,color:#fff
    style BAG fill:#2d3748,color:#fff
    style ENTRY fill:#4a5568,color:#fff
    style TRANS fill:#4a5568,color:#fff
    style ECI fill:#4a5568,color:#fff
    style ECT fill:#718096,color:#fff
    style ENUM fill:#718096,color:#fff
```

      Loading One error. One code. One structure. Every channel. Every language.

How It Works
------------

[](#how-it-works)

### Add flow

[](#add-flow)

 ```
flowchart TD
    A["bag->add(EnumCase, context, params)"] --> B[Read EnumCase getDetails]
    B --> C{Bag hastranslator?}
    C -->|yes| D["translator->translate(description, params)"]
    C -->|no| E[Keep description as raw key]
    D --> F[Create ErrorEntry]
    E --> F
    F --> G[Append to bag]

    style A fill:#2d3748,color:#fff
    style F fill:#4a5568,color:#fff
    style G fill:#276749,color:#fff
```

      Loading ### Two ways to construct a bag

[](#two-ways-to-construct-a-bag)

- **Direct**: `new ErrorBag()` for a raw bag, or `new ErrorBag($translator)` to build one that translates `description` keys at `add()` time.
- **Through `ErrorBagFactory`**: inject the factory once, call `create()` for each fresh bag. The factory carries the (optional) translator, so every bag it produces is pre-wired. Designed for DI auto-wiring — services that depend on the factory never import `MessageTranslatorInterface` themselves.

### Package Structure

[](#package-structure)

```
src/
├── ErrorCodeInterface.php   # Interface for module error enums
├── ErrorCodeTrait.php       # Default implementation via getDetails()
├── ErrorEntry.php           # Readonly DTO — single error
├── ErrorBag.php             # Collector — add, filter, merge, serialize
├── ErrorBagFactory.php      # Produces fresh bags pre-wired with translator
├── ErrorType.php            # 9 error categories
└── HttpStatus.php           # HTTP status codes enum

```

7 files. Single optional dependency on `phpdot/contracts` for the cross-package translator interface.

---

Defining Error Codes
--------------------

[](#defining-error-codes)

### ErrorCodeInterface

[](#errorcodeinterface)

Every module defines a backed string enum implementing this interface:

```
interface ErrorCodeInterface
{
    public function getCode(): string;
    public function getMessage(): string;
    public function getDescription(): string;
    public function getType(): ErrorType;
    public function getHttpStatus(): int;
    public function getDetails(): array;
}
```

### ErrorCodeTrait

[](#errorcodetrait)

Provides the default implementation. The trait reads from `getDetails()` — you only implement one method:

```
trait ErrorCodeTrait
{
    public function getCode(): string       { return $this->value; }
    public function getMessage(): string    { return $this->getDetails()['message']; }
    public function getDescription(): string { return $this->getDetails()['description']; }
    public function getType(): ErrorType    { return $this->getDetails()['type']; }
    public function getHttpStatus(): int    { return $this->getDetails()['httpStatus']; }
}
```

### Module Error Enums

[](#module-error-enums)

Each module owns its errors. No central error file.

```
// User module
enum UserErrors: string implements ErrorCodeInterface
{
    use ErrorCodeTrait;

    case NOT_FOUND     = '00010001';
    case EMAIL_TAKEN   = '00010002';
    case INVALID_EMAIL = '00010003';
    case WEAK_PASSWORD = '00010004';
    case LOCKED        = '00010005';

    public function getDetails(): array
    {
        return match ($this) {
            self::NOT_FOUND => [
                'message'     => 'User not found',
                'description' => 'errors.user.not_found',
                'type'        => ErrorType::NOT_FOUND,
                'httpStatus'  => HttpStatus::NOT_FOUND->value,
            ],
            self::EMAIL_TAKEN => [
                'message'     => 'Email is already taken',
                'description' => 'errors.user.email_taken',
                'type'        => ErrorType::CONFLICT,
                'httpStatus'  => HttpStatus::CONFLICT->value,
            ],
            self::INVALID_EMAIL => [
                'message'     => 'Invalid email address',
                'description' => 'errors.user.invalid_email',
                'type'        => ErrorType::VALIDATION,
                'httpStatus'  => HttpStatus::UNPROCESSABLE_ENTITY->value,
            ],
            self::WEAK_PASSWORD => [
                'message'     => 'Password must be at least 8 characters',
                'description' => 'errors.user.weak_password',
                'type'        => ErrorType::VALIDATION,
                'httpStatus'  => HttpStatus::UNPROCESSABLE_ENTITY->value,
            ],
            self::LOCKED => [
                'message'     => 'Account is locked',
                'description' => 'errors.user.account_locked',
                'type'        => ErrorType::AUTHORIZATION,
                'httpStatus'  => HttpStatus::FORBIDDEN->value,
            ],
        };
    }
}

// Order module — separate file, separate team, no conflicts
enum OrderErrors: string implements ErrorCodeInterface
{
    use ErrorCodeTrait;

    case NOT_FOUND       = '00020001';
    case ALREADY_SHIPPED = '00020002';
    case PAYMENT_FAILED  = '00020003';

    public function getDetails(): array { /* ... */ }
}
```

### Error Code Convention

[](#error-code-convention)

```
Format: MMMMNNNN (8 digits)
        ^^^^              = module ID (0001-9999)
            ^^^^          = error number within module (0001-9999)

Assignments:
    0001 = User / Auth
    0002 = Order
    0003 = Product
    0004 = Payment
    0005 = Event
    ...

```

---

ErrorEntry (The DTO)
--------------------

[](#errorentry-the-dto)

Pure data. No translation, no escaping. Immutable.

```
final readonly class ErrorEntry
{
    public string $code;         // '00010003'
    public string $message;      // 'Invalid email address' (English fallback)
    public string $description;  // 'errors.user.invalid_email' (i18n key)
    public ErrorType $type;      // ErrorType::VALIDATION
    public int $httpStatus;      // 422
    public ?string $context;     // 'email' (field, param, header, service, path)
    public array $params;        // ['min' => 8] (ICU interpolation params)
}

$entry->toArray(); // serializable array
```

---

ErrorBag (The Collector)
------------------------

[](#errorbag-the-collector)

### Adding Errors

[](#adding-errors)

```
$bag = new ErrorBag();

// From module enum
$bag->add(UserErrors::INVALID_EMAIL, 'email');
$bag->add(UserErrors::WEAK_PASSWORD, 'password', ['min' => 8]);

// Raw entry
$bag->addEntry(new ErrorEntry('CUSTOM', 'msg', 'desc', ErrorType::SERVER, 500));

// Chainable
$bag->add(UserErrors::INVALID_EMAIL, 'email')
    ->add(UserErrors::WEAK_PASSWORD, 'password');
```

### Checking Errors

[](#checking-errors)

```
$bag->hasErrors();                          // bool
$bag->hasError(UserErrors::EMAIL_TAKEN);    // check specific code
$bag->count();                              // int
$bag->first();                              // ?ErrorEntry
$bag->all();                                // list
$bag->codes();                              // list — unique codes
```

### Filtering Errors

[](#filtering-errors)

```
// By context (field, param, header, etc.)
$bag->forContext('email');        // list
$bag->forContext('password');     // list
$bag->forContext('Authorization'); // list

// By error type
$bag->ofType(ErrorType::VALIDATION);     // list
$bag->ofType(ErrorType::NOT_FOUND);      // list
$bag->ofType(ErrorType::AUTHENTICATION); // list
```

### Merging Bags

[](#merging-bags)

Combine errors from sub-operations:

```
$userErrors = $userService->validate($data);
$orderErrors = $orderService->validate($data);

$combined = new ErrorBag();
$combined->merge($userErrors)->merge($orderErrors);
```

### HTTP Status

[](#http-status)

Derived from the first error. If empty, returns 500.

```
$bag->getHttpStatus(); // 422 (from first error)
```

### Serialization

[](#serialization)

```
$bag->toArray();
// [
//     ['code' => '00010003', 'message' => '...', 'description' => '...', 'type' => 'validation', 'httpStatus' => 422, 'context' => 'email', 'params' => []],
//     ['code' => '00010004', 'message' => '...', 'description' => '...', 'type' => 'validation', 'httpStatus' => 422, 'context' => 'password', 'params' => ['min' => 8]],
// ]

json_encode(['errors' => $bag->toArray()]);
```

Clear and reset:

```
$bag->clear(); // remove all errors, returns self
```

---

ErrorBagFactory
---------------

[](#errorbagfactory)

Produces fresh `ErrorBag` instances pre-wired with an optional translator. Inject the factory once, call `create()` whenever you need a new bag — translator threading happens automatically.

```
use PHPdot\Error\ErrorBagFactory;

// No translator — produces raw bags
$factory = new ErrorBagFactory();
$bag = $factory->create();
$bag->add(UserErrors::EMAIL_TAKEN, 'email');
$bag->first()->description; // 'errors.user.email_taken' (raw key)

// With translator — every bag carries it
$factory = new ErrorBagFactory($translator);
$bag = $factory->create();
$bag->add(UserErrors::EMAIL_TAKEN, 'email', ['email' => 'omar@phpdot.com']);
$bag->first()->description; // 'The email omar@phpdot.com is already registered.'
```

Each `create()` call returns a new, independent bag — no shared state between calls.

### Auto-wired usage

[](#auto-wired-usage)

When using `phpdot/container`, services that need to produce errors inject the factory directly. The translator is wired into the factory through the container and passed to every bag the factory produces — without the service ever importing `MessageTranslatorInterface`:

```
final class UserService
{
    public function __construct(
        private readonly ErrorBagFactory $bags,
    ) {}

    public function register(string $email): User|ErrorBag
    {
        $bag = $this->bags->create();

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $bag->add(UserErrors::INVALID_EMAIL, 'email');
        }

        return $bag->hasErrors() ? $bag : $this->users->create($email);
    }
}
```

The factory is `#[Scoped]` so each execution unit (request, coroutine) gets its own instance with its own per-request translator.

---

ErrorType (9 Categories)
------------------------

[](#errortype-9-categories)

```
enum ErrorType: string
{
    case VALIDATION     = 'validation';      // input is wrong
    case AUTHENTICATION = 'authentication';  // who are you?
    case AUTHORIZATION  = 'authorization';   // you can't do this
    case NOT_FOUND      = 'not_found';       // doesn't exist
    case CONFLICT       = 'conflict';        // duplicate, version mismatch
    case RATE_LIMIT     = 'rate_limit';      // too many requests
    case TIMEOUT        = 'timeout';         // took too long
    case UNAVAILABLE    = 'unavailable';     // service down
    case SERVER         = 'server';          // unexpected internal error
}
```

The frontend uses the type to decide presentation (red badge for server, yellow for validation, etc.). The error code gives the specific problem.

---

HttpStatus (Typed Enum)
-----------------------

[](#httpstatus-typed-enum)

IDE autocompletion and compile-time safety for HTTP status codes:

```
enum HttpStatus: int
{
    case OK                    = 200;
    case CREATED               = 201;
    case NO_CONTENT            = 204;
    case BAD_REQUEST           = 400;
    case UNAUTHORIZED          = 401;
    case FORBIDDEN             = 403;
    case NOT_FOUND             = 404;
    case CONFLICT              = 409;
    case UNPROCESSABLE_ENTITY  = 422;
    case TOO_MANY_REQUESTS     = 429;
    case INTERNAL_SERVER_ERROR = 500;
    case SERVICE_UNAVAILABLE   = 503;
    // ... 25 codes total
}

// In error enums
'httpStatus' => HttpStatus::NOT_FOUND->value, // 404
```

---

Context
-------

[](#context)

Context is **what the error relates to**. Not just form fields.

```
$errors->add(UserErrors::INVALID_EMAIL, 'email');              // form field
$errors->add(UserErrors::NOT_FOUND, 'user_id');                // route parameter
$errors->add(AuthErrors::INVALID_TOKEN, 'Authorization');       // HTTP header
$errors->add(PaymentErrors::GATEWAY_DOWN, 'stripe');            // service name
$errors->add(OrderErrors::INVALID_ADDRESS, 'address.city');     // nested path
$errors->add(SystemErrors::MAINTENANCE);                        // no context — global
```

Filter by context to show errors next to the right element:

```
$errors->forContext('email');          // errors for the email field
$errors->forContext('Authorization');  // errors for the auth header
```

---

Translation (i18n)
------------------

[](#translation-i18n)

### How Translation Works

[](#how-translation-works)

The error package stores translation keys, not translated text. Translation happens at render time.

```
Error created → description = 'errors.user.email_taken'
                              (this is a translation key, not text)
                                    │
    ┌───────────────────────────────┤
    │                               │
    ▼                               ▼
JSON API                    HTML (server-rendered)
returns the key →           $i18n->trans($error->description)
frontend translates         → "البريد الإلكتروني مستخدم"

```

### ICU Params

[](#icu-params)

Dynamic values for translation interpolation. Works with any ICU-compatible i18n library — [phpdot/i18n](https://github.com/phpdot/i18n) is one option, but not required.

```
$errors->add(ProductErrors::INSUFFICIENT_STOCK, 'quantity', [
    'available' => 5,
    'requested' => 10,
]);

// Translation files:
// en: 'errors.product.insufficient_stock' → 'Only {available} items in stock, you requested {requested}'
// ar: 'errors.product.insufficient_stock' → 'يتوفر {available} عناصر فقط، طلبت {requested}'
```

### Frontend Translation

[](#frontend-translation)

The frontend receives the error code and description key, translates in its own i18n system:

```
response.errors.forEach(error => {
    const translated = i18n.t(error.description, error.params);
    showError(error.context, translated);
});
```

### Server-Side Translation

[](#server-side-translation)

With any translation library:

```
foreach ($bag->all() as $error) {
    $translated = $i18n->trans($error->description, $error->params);
    // Show next to the form field identified by $error->context
}
```

### Auto-Translation via `MessageTranslatorInterface`

[](#auto-translation-via-messagetranslatorinterface)

`ErrorBag` accepts an optional `PHPdot\Contracts\I18n\MessageTranslatorInterface` in its constructor. When wired, every `add()` call replaces the entry's `description` field with the translator's output for the original key + ICU params:

```
use PHPdot\Error\ErrorBag;

$bag = new ErrorBag($translator);   // any class implementing MessageTranslatorInterface
$bag->add(UserErrors::EMAIL_TAKEN, 'email', ['email' => 'omar@phpdot.com']);

$bag->first()->message;       // 'Email is already taken' (always — enum's English string)
$bag->first()->description;   // 'The email omar@phpdot.com is already registered.'
                              // (translated; was the raw key without translator)
```

The `message` field is **never** touched by the bag — it always carries the enum's English string (developer-facing documentation). The `description` field is the runtime-variable one: the raw translation key when no translator is wired, or the translated string when one is.

The bag does not second-guess the translator. Whatever the translator returns — including a `[key]` sentinel for missing keys — goes straight into `description`. Fallback policy is the translator's responsibility, not the bag's.

```
$bag = new ErrorBag();              // no translator
$bag->add(UserErrors::EMAIL_TAKEN);  // description stays as 'errors.user.email_taken'

$bag = new ErrorBag($translator);    // translator wired
$bag->add(UserErrors::EMAIL_TAKEN);  // description becomes the translated string
```

`MessageTranslatorInterface` ships in `phpdot/contracts`. `phpdot/i18n`'s `Translator` already implements it, but any compatible translator works.

---

Output Formats
--------------

[](#output-formats)

### JSON API

[](#json-api)

```
{
    "errors": [
        {
            "code": "00010003",
            "message": "Invalid email address",
            "description": "errors.user.invalid_email",
            "type": "validation",
            "httpStatus": 422,
            "context": "email",
            "params": []
        },
        {
            "code": "00010004",
            "message": "Password must be at least 8 characters",
            "description": "errors.user.weak_password",
            "type": "validation",
            "httpStatus": 422,
            "context": "password",
            "params": {"min": 8}
        }
    ]
}
```

### HTML Forms

[](#html-forms)

```
foreach ($bag->forContext('email') as $error) {
    echo '' . $i18n->trans($error->description, $error->params) . '';
}
```

### CLI

[](#cli)

```
[00010003] Invalid email address (context: email)
[00010004] Password must be at least 8 characters (context: password)

```

### WebSocket

[](#websocket)

Same `$bag->toArray()` serialized as JSON. Identical structure to the API.

---

Real-World Usage
----------------

[](#real-world-usage)

### Service Validation

[](#service-validation)

```
final class UserService
{
    public function register(string $email, string $password): User|ErrorBag
    {
        $errors = new ErrorBag();

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors->add(UserErrors::INVALID_EMAIL, 'email');
        }

        if (strlen($password) < 8) {
            $errors->add(UserErrors::WEAK_PASSWORD, 'password', ['min' => 8]);
        }

        if ($errors->hasErrors()) {
            return $errors;
        }

        if ($this->users->emailExists($email)) {
            $errors->add(UserErrors::EMAIL_TAKEN, 'email');
            return $errors;
        }

        return $this->users->create($email, $password);
    }
}
```

### Cross-Module Merge

[](#cross-module-merge)

```
$userErrors = $userService->validate($data);
$addressErrors = $addressService->validate($data['address']);

$errors = new ErrorBag();
$errors->merge($userErrors)->merge($addressErrors);

if ($errors->hasErrors()) {
    return response()->json(['errors' => $errors->toArray()], $errors->getHttpStatus());
}
```

### Frontend Grouping

[](#frontend-grouping)

```
$grouped = [];
foreach ($bag->all() as $error) {
    $key = $error->context ?? '_global';
    $grouped[$key][] = $error->toArray();
}
// $grouped['email'] → [{code: '00010003', ...}, {code: '00010002', ...}]
// $grouped['password'] → [{code: '00010004', ...}]
// $grouped['_global'] → [{code: '00060001', ...}]
```

---

API Reference
-------------

[](#api-reference)

### ErrorCodeInterface API

[](#errorcodeinterface-api)

```
interface ErrorCodeInterface

getCode(): string
getMessage(): string
getDescription(): string
getType(): ErrorType
getHttpStatus(): int
getDetails(): array{message: string, description: string, type: ErrorType, httpStatus: int}

```

### ErrorCodeTrait API

[](#errorcodetrait-api)

```
trait ErrorCodeTrait

getCode(): string            // returns $this->value
getMessage(): string         // returns getDetails()['message']
getDescription(): string     // returns getDetails()['description']
getType(): ErrorType         // returns getDetails()['type']
getHttpStatus(): int         // returns getDetails()['httpStatus']

```

### ErrorEntry API

[](#errorentry-api)

```
final readonly class ErrorEntry

__construct(
    public string    $code,
    public string    $message,
    public string    $description,
    public ErrorType $type,
    public int       $httpStatus,
    public ?string   $context = null,
    public array $params = [],
)

toArray(): array{code, message, description, type, httpStatus, context, params}

```

### ErrorBag API

[](#errorbag-api)

```
final class ErrorBag

__construct(?MessageTranslatorInterface $translator = null)
add(ErrorCodeInterface $error, ?string $context = null, array $params = []): self
addEntry(ErrorEntry $entry): self
hasErrors(): bool
hasError(ErrorCodeInterface $error): bool
all(): list
first(): ?ErrorEntry
forContext(string $context): list
ofType(ErrorType $type): list
count(): int
merge(self $other): self
clear(): self
getHttpStatus(): int
codes(): list
toArray(): list

```

### ErrorType API

[](#errortype-api)

```
enum ErrorType: string

VALIDATION     = 'validation'
AUTHENTICATION = 'authentication'
AUTHORIZATION  = 'authorization'
NOT_FOUND      = 'not_found'
CONFLICT       = 'conflict'
RATE_LIMIT     = 'rate_limit'
TIMEOUT        = 'timeout'
UNAVAILABLE    = 'unavailable'
SERVER         = 'server'

```

### HttpStatus API

[](#httpstatus-api)

```
enum HttpStatus: int

```

CaseValue`OK`200`CREATED`201`ACCEPTED`202`NO_CONTENT`204`MOVED_PERMANENTLY`301`FOUND`302`NOT_MODIFIED`304`TEMPORARY_REDIRECT`307`PERMANENT_REDIRECT`308`BAD_REQUEST`400`UNAUTHORIZED`401`FORBIDDEN`403`NOT_FOUND`404`METHOD_NOT_ALLOWED`405`CONFLICT`409`GONE`410`PAYLOAD_TOO_LARGE`413`UNSUPPORTED_MEDIA`415`UNPROCESSABLE_ENTITY`422`TOO_MANY_REQUESTS`429`INTERNAL_SERVER_ERROR`500`BAD_GATEWAY`502`SERVICE_UNAVAILABLE`503`GATEWAY_TIMEOUT`504---

License
-------

[](#license)

MIT

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance92

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~22 days

Total

5

Last Release

4d ago

PHP version history (2 changes)v1.0.0PHP &gt;=8.3

v1.2.1PHP &gt;=8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/62e82421bda4b5d6ba9a47ba6d88caca060dcd0d1a2862f351f3a97657385db0?d=identicon)[phpdot](/maintainers/phpdot)

---

Tags

validationi18nerrorstructurederror codeerror-bag

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/phpdot-error/health.svg)

```
[![Health](https://phpackages.com/badges/phpdot-error/health.svg)](https://phpackages.com/packages/phpdot-error)
```

###  Alternatives

[composer/semver

Version comparison library that offers utilities, version constraint parsing and validation.

3.3k522.3M994](/packages/composer-semver)[giggsey/libphonenumber-for-php

A library for parsing, formatting, storing and validating international phone numbers, a PHP Port of Google's libphonenumber.

5.0k159.6M527](/packages/giggsey-libphonenumber-for-php)[respect/validation

The most awesome validation engine ever created for PHP

6.0k39.9M414](/packages/respect-validation)[propaganistas/laravel-phone

Adds phone number functionality to Laravel based on Google's libphonenumber API.

3.0k39.7M146](/packages/propaganistas-laravel-phone)[opis/json-schema

Json Schema Validator for PHP

65243.6M305](/packages/opis-json-schema)[apy/jsfv-bundle

Symfony2 Javascript Form Validation Bundle with localisation support

92773.7k](/packages/apy-jsfv-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
