Skip to content

Commit fd67319

Browse files
committed
Initial commit
0 parents  commit fd67319

50 files changed

Lines changed: 2041 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Exceptions CI
2+
3+
on:
4+
push:
5+
branches: [ main, dev ]
6+
pull_request:
7+
branches: [ main, dev ]
8+
9+
jobs:
10+
quality:
11+
name: Quality Checks (PHP ${{ matrix.php }})
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
php: [ "8.1", "8.2", "8.3", "8.4" ]
18+
19+
steps:
20+
# -------------------------------------------------
21+
# 1) Checkout
22+
# -------------------------------------------------
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
# -------------------------------------------------
27+
# 2) Setup PHP
28+
# -------------------------------------------------
29+
- name: Setup PHP
30+
uses: shivammathur/setup-php@v2
31+
with:
32+
php-version: ${{ matrix.php }}
33+
tools: composer
34+
coverage: none
35+
36+
# -------------------------------------------------
37+
# 3) Validate composer.json
38+
# -------------------------------------------------
39+
- name: Validate composer.json
40+
run: composer validate --strict
41+
42+
# -------------------------------------------------
43+
# 4) Install dependencies
44+
# -------------------------------------------------
45+
- name: Install dependencies
46+
run: composer install --no-interaction --prefer-dist --no-progress
47+
48+
# -------------------------------------------------
49+
# 5) Dump optimized autoload
50+
# -------------------------------------------------
51+
- name: Optimize autoload
52+
run: composer dump-autoload --optimize
53+
54+
# -------------------------------------------------
55+
# 6) Run PHPStan (Level Max)
56+
# -------------------------------------------------
57+
- name: Run PHPStan
58+
run: composer analyse
59+
60+
# -------------------------------------------------
61+
# 7) Syntax Check (extra safety)
62+
# -------------------------------------------------
63+
- name: Lint PHP files
64+
run: |
65+
find src -type f -name "*.php" -print0 | xargs -0 -n1 php -l

BOOK/01_Introduction.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Introduction
2+
3+
`maatify/exceptions` is a strictly typed, immutable taxonomy for PHP application errors.
4+
5+
## Purpose
6+
7+
The primary goal of this library is to solve "Taxonomy Drift"—the tendency for applications to lose semantic meaning in their error handling over time.
8+
9+
By enforcing a strict taxonomy and guarding override capabilities, this library ensures that:
10+
1. **System errors remain System errors** (even when wrapped).
11+
2. **Client errors remain Client errors** (even when re-thrown).
12+
3. **Monitoring tools receive accurate signals** about system health.
13+
14+
## Philosophy
15+
16+
* **Exceptions should be semantic:** "User not found" is a different category than "Database offline".
17+
* **Taxonomy should be immutable:** A developer should not be able to arbitrarily change the category of an exception at runtime.
18+
* **Severity should never be downgraded:** Critical failures must always bubble up as critical failures.
19+
20+
## Scope
21+
22+
This library provides:
23+
* A base `MaatifyException` class.
24+
* Strictly typed Enums for Categories and Error Codes.
25+
* Concrete exception classes for common scenarios (Validation, Auth, System, etc.).
26+
* Logic for safe overrides and escalation.
27+
28+
## Non-Goals
29+
30+
This library does *not* provide:
31+
* HTTP handling or middleware (it is framework-agnostic).
32+
* Logging implementations (it is PSR-3 compatible but does not implement a logger).
33+
* Automatic error reporting (it provides the *data* for reporting, not the transport).

BOOK/02_Architecture.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Architecture
2+
3+
`maatify/exceptions` is built on a simple yet strict architecture:
4+
5+
## Class Hierarchy
6+
7+
1. **`Throwable` (PHP Core Interface)**
8+
* **`ApiAwareExceptionInterface` (Contract)** - The public contract for all application exceptions.
9+
* **`MaatifyException` (Abstract Base Class)** - Implements `ApiAwareExceptionInterface` and enforces strict taxonomy rules.
10+
* **Concrete Exception Families** (e.g., `SystemMaatifyException`, `ValidationMaatifyException`)
11+
* **Specific Exceptions** (e.g., `DatabaseConnectionMaatifyException`, `InvalidArgumentMaatifyException`)
12+
13+
## Key Design Invariants
14+
15+
1. **Immutability:** Exception categories are defined by the class type and cannot be changed at runtime.
16+
2. **Strict Taxonomy:** Only predefined `ErrorCategoryEnum` values are allowed.
17+
3. **Strict Typing:** All exception handling relies on PHP strict types.
18+
4. **Escalation Logic:** Automatic severity calculation logic resides in the base `MaatifyException` constructor.
19+
20+
## ApiAwareExceptionInterface
21+
22+
The `ApiAwareExceptionInterface` defines the core contract for all exceptions:
23+
24+
```php
25+
interface ApiAwareExceptionInterface extends Throwable
26+
{
27+
public function getHttpStatus(): int;
28+
public function getErrorCode(): ErrorCodeEnum;
29+
public function getCategory(): ErrorCategoryEnum;
30+
public function isSafe(): bool;
31+
public function getMeta(): array;
32+
public function isRetryable(): bool;
33+
}
34+
```
35+
36+
* **`isSafe()`**: Indicates if the exception message can be safely exposed to the client (e.g., Validation Errors).
37+
* **`isRetryable()`**: Indicates if the request can be retried immediately (e.g., Rate Limiting).
38+
39+
This interface guarantees that any conforming exception can be processed by a generic error handler (e.g., converting to JSON response).

BOOK/03_Taxonomy.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Taxonomy
2+
3+
The core of `maatify/exceptions` is its strict taxonomy system.
4+
5+
## ErrorCategoryEnum
6+
7+
The `ErrorCategoryEnum` defines the 9 core families of exceptions.
8+
9+
| Category | Description |
10+
|------------------|-----------------------------------------------------|
11+
| `SYSTEM` | Critical system failures (database, network, file). |
12+
| `RATE_LIMIT` | Too many requests (client throttling). |
13+
| `AUTHENTICATION` | Authentication failures (login required). |
14+
| `AUTHORIZATION` | Forbidden access (permission required). |
15+
| `VALIDATION` | Invalid input data (bad request). |
16+
| `BUSINESS_RULE` | Domain-specific logic violation. |
17+
| `CONFLICT` | Resource conflict (duplicate entry). |
18+
| `NOT_FOUND` | Resource not found (404). |
19+
| `UNSUPPORTED` | Unsupported operation or method. |
20+
21+
## ErrorCodeEnum
22+
23+
The `ErrorCodeEnum` provides distinct error codes for specific scenarios.
24+
25+
| Code | Usage |
26+
|------------------------------|-----------------------------------------------------------------|
27+
| `MAATIFY_ERROR` | Generic system error. |
28+
| `INVALID_ARGUMENT` | Generic validation error. |
29+
| `BUSINESS_RULE_VIOLATION` | Generic business logic error. |
30+
| `RESOURCE_NOT_FOUND` | Resource not found. |
31+
| `CONFLICT` | Resource conflict. |
32+
| `ENTITY_IN_USE` | Cannot delete entity because it is in use. |
33+
| `UNAUTHORIZED` | Authentication required. |
34+
| `SESSION_EXPIRED` | Session expired. |
35+
| `AUTH_STATE_VIOLATION` | Account state prevents action (e.g., locked, password expired). |
36+
| `RECOVERY_LOCKED` | Recovery mechanism locked due to excessive attempts. |
37+
| `FORBIDDEN` | Access denied. |
38+
| `UNSUPPORTED_OPERATION` | Operation not supported. |
39+
| `DATABASE_CONNECTION_FAILED` | Database connection error. |
40+
| `TOO_MANY_REQUESTS` | Rate limit exceeded. |
41+
42+
## Strict Mapping
43+
44+
The library strictly enforces which Error Codes can be used with which Categories. For example, you cannot use `DATABASE_CONNECTION_FAILED` with a `VALIDATION` category. Attempting to do so will result in a `LogicException`.

BOOK/04_Exception_Families.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Exception Families
2+
3+
Exceptions are categorized into 9 main families, each backed by an abstract base class.
4+
5+
## 1. SystemMaatifyException
6+
7+
* **Category:** `SYSTEM`
8+
* **Default Status:** 500
9+
* **Usage:** For critical failures like database errors, filesystem failures, or external API outages.
10+
* **Concrete:** `DatabaseConnectionMaatifyException`
11+
12+
## 2. RateLimitMaatifyException
13+
14+
* **Category:** `RATE_LIMIT`
15+
* **Default Status:** 429
16+
* **Default `isRetryable()`:** `true`
17+
* **Usage:** When a client exceeds the allowed request rate.
18+
* **Concrete:** `TooManyRequestsMaatifyException`
19+
20+
## 3. AuthenticationMaatifyException
21+
22+
* **Category:** `AUTHENTICATION`
23+
* **Default Status:** 401
24+
* **Usage:** When a user is not logged in or has an invalid session.
25+
* **Concrete:** `UnauthorizedMaatifyException`, `SessionExpiredMaatifyException`
26+
27+
## 4. AuthorizationMaatifyException
28+
29+
* **Category:** `AUTHORIZATION`
30+
* **Default Status:** 403
31+
* **Usage:** When a user is logged in but lacks permission.
32+
* **Concrete:** `ForbiddenMaatifyException`
33+
34+
## 5. ValidationMaatifyException
35+
36+
* **Category:** `VALIDATION`
37+
* **Default Status:** 400
38+
* **Usage:** For invalid client input (e.g., malformed email, missing field).
39+
* **Concrete:** `InvalidArgumentMaatifyException`
40+
41+
## 6. BusinessRuleMaatifyException
42+
43+
* **Category:** `BUSINESS_RULE`
44+
* **Default Status:** 422
45+
* **Usage:** For domain-specific logic (e.g., "Cannot cancel shipped order").
46+
* **Concrete:** `BusinessRuleMaatifyException` (Abstract)
47+
48+
## 7. ConflictMaatifyException
49+
50+
* **Category:** `CONFLICT`
51+
* **Default Status:** 409
52+
* **Usage:** For duplicate resource creation or data inconsistency.
53+
* **Concrete:** `GenericConflictMaatifyException`
54+
55+
## 8. NotFoundMaatifyException
56+
57+
* **Category:** `NOT_FOUND`
58+
* **Default Status:** 404
59+
* **Usage:** When a requested resource does not exist.
60+
* **Concrete:** `ResourceNotFoundMaatifyException`
61+
62+
## 9. UnsupportedMaatifyException
63+
64+
* **Category:** `UNSUPPORTED`
65+
* **Default Status:** 409
66+
* **Usage:** When an operation is valid but not supported in the current context.
67+
* **Concrete:** `UnsupportedOperationMaatifyException`

BOOK/05_Override_Rules.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Override Rules
2+
3+
The `MaatifyException` base class allows developers to override default metadata (HTTP Status, Error Code) but enforces strict guardrails.
4+
5+
## 1. Category Immutability
6+
7+
You **cannot** override the category of an exception.
8+
9+
* `ValidationMaatifyException` will always be `VALIDATION`.
10+
* `SystemMaatifyException` will always be `SYSTEM`.
11+
12+
This prevents "Taxonomy Drift" where exceptions lose their semantic meaning.
13+
14+
## 2. Error Code Constraints
15+
16+
If you provide an `$errorCodeOverride` in the constructor:
17+
18+
1. The code **must exist** in the `ALLOWED_ERROR_CODES` mapping for that category.
19+
2. If it does not match, a `LogicException` is thrown immediately.
20+
21+
### Fallback Semantics
22+
23+
The default policy validation is permissive for unconfigured categories:
24+
25+
* **Unconfigured category:** Allows **all** codes.
26+
* **Configured category with empty list:** Allows **all** codes.
27+
* **Configured category with codes:** Strictly enforces the allowed list.
28+
29+
**Example:**
30+
* `ValidationMaatifyException` allows `INVALID_ARGUMENT`.
31+
* It forbids `DATABASE_CONNECTION_FAILED`.
32+
33+
*(Note: Since `ValidationMaatifyException` is abstract, these rules apply to any concrete class extending it.)*
34+
35+
## 3. HTTP Status Class Guard
36+
37+
If you provide an `$httpStatusOverride`:
38+
39+
1. It **must match the default status class** (Client Error vs Server Error).
40+
2. `4xx` defaults can be overridden with other `4xx` codes.
41+
3. `5xx` defaults can be overridden with other `5xx` codes.
42+
4. **Cross-class overrides are forbidden.** (e.g., 400 -> 500).
43+
44+
This ensures that a client-side error (Validation) never accidentally reports a server-side failure (System) to monitoring tools.
45+
46+
## 4. Policy Customization
47+
48+
You can inject a custom `ErrorPolicyInterface` to override default rules (e.g., allow extra codes).
49+
50+
```php
51+
// Create a policy with custom rules
52+
$customPolicy = DefaultErrorPolicy::withOverrides(
53+
allowedOverrides: ['VALIDATION' => ['MY_CUSTOM_CODE']]
54+
);
55+
56+
// Inject globally (PROCESS-WIDE)
57+
MaatifyException::setGlobalPolicy($customPolicy);
58+
MaatifyException::setGlobalEscalationPolicy($customEscalationPolicy);
59+
```
60+
61+
### Resolution Precedence
62+
63+
The library resolves policies in the following order:
64+
65+
1. **Escalated:** (Highest Priority) Logic derived from previous exception wrapping.
66+
2. **Override:** Instance-specific overrides passed to the constructor.
67+
3. **Default:** The active `ErrorPolicyInterface` (Global or Default).
68+
69+
### ⚠️ Warning: Long-Running Processes
70+
71+
In persistent environments (e.g., **Swoole**, **RoadRunner**), static global state persists across requests. Global policy setters (`setGlobalPolicy`, `setGlobalEscalationPolicy`) are **PROCESS-WIDE**.
72+
73+
* **Risk:** Setting a global policy in one request may affect subsequent requests.
74+
* **Best Practice:** Set global policies only during application bootstrap, before handling any requests.
75+
* **Cleanup:** Use `MaatifyException::resetGlobalPolicies()` to clear all static overrides.

BOOK/06_Escalation_Protection.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Escalation Protection
2+
3+
`maatify/exceptions` features a deterministic escalation mechanism.
4+
5+
## The Problem: Swallowed Errors
6+
7+
A common pattern in PHP is to wrap exceptions:
8+
9+
```php
10+
try {
11+
$db->connect(); // Throws 503 System Error
12+
} catch (Exception $e) {
13+
// Attempting to wrap in a business rule exception
14+
throw new class("Cannot process request", 0, $e) extends BusinessRuleMaatifyException {};
15+
}
16+
```
17+
18+
This effectively **hides the system failure**. Monitoring tools see a 422 (Business Rule) instead of a critical 503 (System Failure).
19+
20+
## The Solution: Automatic Escalation
21+
22+
When wrapping an exception using `MaatifyException`:
23+
24+
1. **Category Severity Check:** The library compares the severity of the new exception against the previous one.
25+
2. **HTTP Status Check:** The library compares the HTTP status codes.
26+
3. **Result:** The final exception adopts the **higher severity** category and status.
27+
28+
### Escalation Logic
29+
30+
* **Category:** If `previous_severity > current_severity`, the category is escalated.
31+
* **HTTP Status:** Uses `max(current, previous)` to ensure the highest error class is preserved (e.g., 500 overrides 400).
32+
* **Unknown Categories:** `severity()` returns **0** for unknown categories, treating them as lowest priority.
33+
34+
### Severity Ranking (High to Low)
35+
36+
1. `SYSTEM` (90)
37+
2. `RATE_LIMIT` (80)
38+
3. `AUTHENTICATION` (70)
39+
4. `AUTHORIZATION` (60)
40+
5. `VALIDATION` (50)
41+
6. `BUSINESS_RULE` (40)
42+
7. `CONFLICT` (30)
43+
8. `NOT_FOUND` (20)
44+
9. `UNSUPPORTED` (10)
45+
46+
### Example
47+
48+
* **Original:** `SystemMaatifyException` (Severity 90, Status 503)
49+
* **Wrapper:** `BusinessRuleMaatifyException` (Severity 40, Status 422)
50+
51+
**Final Outcome:**
52+
* **Category:** `SYSTEM` (Escalated from BusinessRule)
53+
* **Status:** 503 (Escalated from 422)
54+
55+
This guarantees that critical errors are **never masked**.

0 commit comments

Comments
 (0)