# Exception Hierarchy Pattern Guide

Comprehensive guide for creating module-specific exception hierarchies in the Custom PHP Framework.

## Overview

The framework uses a **module-based exception hierarchy** pattern that provides:
- **Type-safe exception handling** with domain-specific exceptions
- **Rich error context** with ErrorCode integration
- **Clean API** using factory methods
- **Automatic HTTP status mapping** and retry strategies
- **Structured logging** with severity levels

## Architecture

```
FrameworkException (Base)
    └── ModuleException (e.g., QueueException)
        ├── SpecificException1 (e.g., JobNotFoundException)
        ├── SpecificException2 (e.g., ChainNotFoundException)
        └── SpecificException3 (e.g., InvalidChainStateException)
```

### Key Components

1. **FrameworkException**: Base exception with ErrorCode and ExceptionContext
2. **ModuleException**: Module-specific base (e.g., `QueueException`, `DatabaseException`)
3. **Specific Exceptions**: Domain-specific exceptions with factory methods
4. **ErrorCode Enum**: Category-specific error codes (e.g., `QueueErrorCode`, `DatabaseErrorCode`)

## Creating a Module Exception Hierarchy

### Step 1: Create Module Base Exception

```php
<?php

declare(strict_types=1);

namespace App\Framework\YourModule\Exceptions;

use App\Framework\Exception\FrameworkException;

/**
 * Base exception for YourModule
 *
 * All YourModule-related exceptions should extend this class.
 */
class YourModuleException extends FrameworkException
{
}
```

**Location**: `src/Framework/YourModule/Exceptions/YourModuleException.php`

**Naming Convention**: `{ModuleName}Exception`

### Step 2: Add Module-Specific Error Codes

Add error codes to the appropriate ErrorCode enum (or create a new one):

```php
<?php

namespace App\Framework\Exception\Core;

enum YourModuleErrorCode: string implements ErrorCode
{
    case RESOURCE_NOT_FOUND = 'MODULE001';
    case INVALID_STATE = 'MODULE002';
    case OPERATION_FAILED = 'MODULE003';

    public function getValue(): string
    {
        return $this->value;
    }

    public function getCategory(): string
    {
        return 'MODULE';
    }

    public function getNumericCode(): int
    {
        return (int) substr($this->value, -3);
    }

    public function getSeverity(): ErrorSeverity
    {
        return match($this) {
            self::RESOURCE_NOT_FOUND => ErrorSeverity::ERROR,
            self::INVALID_STATE => ErrorSeverity::WARNING,
            self::OPERATION_FAILED => ErrorSeverity::CRITICAL,
        };
    }

    public function getDescription(): string
    {
        return match($this) {
            self::RESOURCE_NOT_FOUND => 'Resource not found in module',
            self::INVALID_STATE => 'Operation not allowed in current state',
            self::OPERATION_FAILED => 'Module operation failed',
        };
    }

    public function getRecoveryHint(): string
    {
        return match($this) {
            self::RESOURCE_NOT_FOUND => 'Verify resource ID and check if resource exists',
            self::INVALID_STATE => 'Check current state and verify operation prerequisites',
            self::OPERATION_FAILED => 'Review logs and retry operation',
        };
    }

    public function isRecoverable(): bool
    {
        return match($this) {
            self::OPERATION_FAILED => false,
            default => true,
        };
    }

    public function getRetryAfterSeconds(): ?int
    {
        return match($this) {
            self::OPERATION_FAILED => 60,
            default => null,
        };
    }
}
```

**Location**: `src/Framework/Exception/Core/YourModuleErrorCode.php`

**Error Code Format**: `{CATEGORY}{NUMBER}` (e.g., `MODULE001`, `MODULE002`)

**Severity Levels**:
- `CRITICAL`: System failure, immediate action required
- `ERROR`: Operation failed, attention needed
- `WARNING`: Potential issue, monitor
- `INFO`: Informational, no action needed
- `DEBUG`: Debugging information

### Step 3: Create Specific Exceptions

Create domain-specific exceptions with factory methods:

```php
<?php

declare(strict_types=1);

namespace App\Framework\YourModule\Exceptions;

use App\Framework\Exception\Core\YourModuleErrorCode;
use App\Framework\Exception\ExceptionContext;

/**
 * Exception thrown when a resource is not found
 */
final class ResourceNotFoundException extends YourModuleException
{
    public static function byId(ResourceId $id): self
    {
        $context = ExceptionContext::forOperation('resource.lookup', 'ResourceService')
            ->withData([
                'resource_id' => $id->toString(),
                'search_type' => 'by_id',
            ]);

        return self::create(
            YourModuleErrorCode::RESOURCE_NOT_FOUND,
            "Resource with ID '{$id->toString()}' not found",
            $context
        );
    }

    public static function byName(string $name): self
    {
        $context = ExceptionContext::forOperation('resource.lookup', 'ResourceService')
            ->withData([
                'resource_name' => $name,
                'search_type' => 'by_name',
            ]);

        return self::create(
            YourModuleErrorCode::RESOURCE_NOT_FOUND,
            "Resource with name '{$name}' not found",
            $context
        );
    }
}
```

**Location**: `src/Framework/YourModule/Exceptions/ResourceNotFoundException.php`

### Step 4: Use Exceptions in Code

Replace generic exceptions with domain-specific ones:

```php
// ❌ Before: Generic RuntimeException
public function findResource(ResourceId $id): Resource
{
    $resource = $this->repository->find($id);

    if ($resource === null) {
        throw new \RuntimeException("Resource {$id->toString()} not found");
    }

    return $resource;
}

// ✅ After: Domain-specific exception with factory method
use App\Framework\YourModule\Exceptions\ResourceNotFoundException;

public function findResource(ResourceId $id): Resource
{
    $resource = $this->repository->find($id);

    if ($resource === null) {
        throw ResourceNotFoundException::byId($id);
    }

    return $resource;
}
```

## Factory Method Patterns

### Single Factory Method

For simple cases with one creation scenario:

```php
final class ConfigurationMissingException extends YourModuleException
{
    public static function forKey(string $key): self
    {
        $context = ExceptionContext::forOperation('config.load', 'ConfigService')
            ->withData(['config_key' => $key]);

        return self::create(
            YourModuleErrorCode::CONFIG_MISSING,
            "Configuration key '{$key}' is missing",
            $context
        );
    }
}
```

### Multiple Factory Methods

For different creation scenarios:

```php
final class InvalidStateException extends YourModuleException
{
    public static function notReady(string $resourceId, string $currentState): self
    {
        $context = ExceptionContext::forOperation('resource.start', 'ResourceService')
            ->withData([
                'resource_id' => $resourceId,
                'current_state' => $currentState,
                'required_state' => 'ready',
            ]);

        return self::create(
            YourModuleErrorCode::INVALID_STATE,
            "Resource '{$resourceId}' is not ready (current: {$currentState})",
            $context
        );
    }

    public static function alreadyCompleted(string $resourceId): self
    {
        $context = ExceptionContext::forOperation('resource.modify', 'ResourceService')
            ->withData([
                'resource_id' => $resourceId,
                'current_state' => 'completed',
            ]);

        return self::create(
            YourModuleErrorCode::INVALID_STATE,
            "Resource '{$resourceId}' is already completed",
            $context
        );
    }
}
```

### Type-Safe Factory Methods

Accept Value Objects for type safety:

```php
final class UserNotFoundException extends AuthModuleException
{
    public static function byId(UserId $userId): self
    {
        $context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
            ->withData(['user_id' => $userId->toString()]);

        return self::create(
            AuthErrorCode::USER_NOT_FOUND,
            "User with ID '{$userId->toString()}' not found",
            $context
        );
    }

    public static function byEmail(Email $email): self
    {
        $context = ExceptionContext::forOperation('user.lookup', 'UserRepository')
            ->withData(['email' => $email->getMasked()]);

        return self::create(
            AuthErrorCode::USER_NOT_FOUND,
            "User with email not found",
            $context
        );
    }
}
```

## ExceptionContext Best Practices

### Operation and Component

Always specify operation and component for traceability:

```php
$context = ExceptionContext::forOperation(
    'order.process',      // Operation: what was being attempted
    'OrderService'        // Component: which class/service
);
```

### Context Data

Add relevant data for debugging:

```php
$context->withData([
    'order_id' => $orderId,
    'customer_id' => $customerId,
    'total_amount' => $totalAmount,
    'payment_method' => $paymentMethod,
]);
```

### Debug Information

Add internal details for development (automatically filtered in production):

```php
$context->withDebug([
    'query' => $sql,
    'bind_params' => $params,
    'execution_time_ms' => $executionTime,
]);
```

### Metadata

Add structured metadata:

```php
$context->withMetadata([
    'attempt' => $attemptNumber,
    'max_attempts' => $maxAttempts,
    'retry_strategy' => 'exponential_backoff',
]);
```

## Exception Catching Patterns

### Catch Specific Exception

```php
try {
    $resource = $this->resourceService->find($id);
} catch (ResourceNotFoundException $e) {
    // Handle specific exception
    return $this->notFoundResponse($e->getMessage());
}
```

### Catch Module-Level Exception

```php
try {
    $result = $this->moduleService->process($data);
} catch (YourModuleException $e) {
    // Handle any exception from this module
    $this->logger->error('Module operation failed', [
        'error_code' => $e->getErrorCode()->getValue(),
        'message' => $e->getMessage(),
    ]);
    throw $e;
}
```

### Catch by Category

```php
try {
    $result = $this->service->execute();
} catch (FrameworkException $e) {
    if ($e->isCategory('DB')) {
        // Handle database errors
        return $this->databaseErrorResponse();
    }

    if ($e->isCategory('QUEUE')) {
        // Handle queue errors
        return $this->queueErrorResponse();
    }

    throw $e;
}
```

### Catch by Specific ErrorCode

```php
try {
    $resource = $this->service->find($id);
} catch (FrameworkException $e) {
    if ($e->isErrorCode(YourModuleErrorCode::RESOURCE_NOT_FOUND)) {
        // Handle specific error code
        return $this->notFoundResponse();
    }

    throw $e;
}
```

## ErrorHandler Integration

The ErrorHandler automatically leverages the exception hierarchy:

### Automatic HTTP Status Mapping

```php
// ErrorCode category determines HTTP status:
// AUTH -> 401 Unauthorized
// AUTHZ -> 403 Forbidden
// VAL -> 400 Bad Request
// DB/QUEUE/CACHE -> 500 Internal Server Error
```

### Automatic Severity-Based Logging

```php
// ErrorCode.getSeverity() determines log level:
// CRITICAL -> critical log
// ERROR -> error log
// WARNING -> warning log
// INFO -> info log
// DEBUG -> debug log
```

### Automatic Retry-After Headers

```php
// ErrorCode.getRetryAfterSeconds() adds Retry-After header:
if ($errorCode->getRetryAfterSeconds() !== null) {
    $response->headers['Retry-After'] = $retryAfter;
}
```

### Recovery Hints in Debug Mode

```php
// ErrorCode.getRecoveryHint() included in debug responses:
$metadata['recovery_hint'] = $errorCode->getRecoveryHint();
```

## Migration Checklist

When migrating a module to the new exception hierarchy:

- [ ] Create module base exception (`{Module}Exception extends FrameworkException`)
- [ ] Add module-specific error codes to `{Module}ErrorCode` enum
- [ ] Implement all required ErrorCode methods (getValue, getCategory, getSeverity, etc.)
- [ ] Create specific exceptions with factory methods
- [ ] Replace `RuntimeException` with domain-specific exceptions
- [ ] Replace `InvalidArgumentException` in business logic (keep for constructor validation)
- [ ] Add comprehensive ExceptionContext to all exceptions
- [ ] Test exception creation and ErrorHandler integration
- [ ] Update module documentation

## Common Patterns

### Infrastructure Exceptions

For missing dependencies or configuration:

```php
final class RedisExtensionNotLoadedException extends QueueException
{
    public static function create(): self
    {
        $context = ExceptionContext::forOperation('queue.init', 'QueueInitializer')
            ->withData([
                'required_extension' => 'redis',
                'loaded_extensions' => get_loaded_extensions(),
                'fallback_available' => 'FileQueue',
            ]);

        return self::fromContext(
            'Redis PHP extension is not loaded',
            $context,
            QueueErrorCode::WORKER_UNAVAILABLE
        );
    }
}
```

### State Transition Exceptions

For invalid state transitions:

```php
final class AllStepsCompletedException extends QueueException
{
    public static function forJob(string $jobId, int $totalSteps): self
    {
        $context = ExceptionContext::forOperation('step.complete', 'StepProgressTracker')
            ->withData([
                'job_id' => $jobId,
                'total_steps' => $totalSteps,
                'current_step_index' => $totalSteps,
            ]);

        return self::create(
            QueueErrorCode::INVALID_STATE,
            "All {$totalSteps} steps have already been completed for job '{$jobId}'",
            $context
        );
    }
}
```

### Validation Exceptions

For business rule violations:

```php
final class CircularDependencyException extends QueueException
{
    public static function inChain(string $chainId, array $circularDependencies = []): self
    {
        $context = ExceptionContext::forOperation('chain.validate', 'JobChainExecutionCoordinator')
            ->withData([
                'chain_id' => $chainId,
                'circular_dependencies' => $circularDependencies,
            ]);

        return self::create(
            QueueErrorCode::CIRCULAR_DEPENDENCY,
            "Chain '{$chainId}' has circular dependencies",
            $context
        );
    }
}
```

## Anti-Patterns to Avoid

### ❌ Don't Use FrameworkException::create() Directly

```php
// ❌ BAD: Clutters business logic
throw FrameworkException::create(
    ErrorCode::RESOURCE_NOT_FOUND,
    "Resource not found"
)->withContext(
    ExceptionContext::forOperation('resource.lookup', 'ResourceService')
        ->withData(['resource_id' => $id])
);

// ✅ GOOD: Clean factory method
throw ResourceNotFoundException::byId($id);
```

### ❌ Don't Create Anonymous Exceptions

```php
// ❌ BAD: Anonymous exception, no reusability
throw new class extends FrameworkException {
    // ...
};

// ✅ GOOD: Named exception class
throw ResourceNotFoundException::byId($id);
```

### ❌ Don't Mix Concerns in Exception Names

```php
// ❌ BAD: Too specific, hard to reuse
final class UserNotFoundInDatabaseByEmailException extends AuthException { }

// ✅ GOOD: Generic, multiple factory methods
final class UserNotFoundException extends AuthException
{
    public static function byId(UserId $id): self { }
    public static function byEmail(Email $email): self { }
}
```

### ❌ Don't Skip ExceptionContext

```php
// ❌ BAD: No context
return self::create(
    ErrorCode::RESOURCE_NOT_FOUND,
    "Resource not found"
);

// ✅ GOOD: Rich context
$context = ExceptionContext::forOperation('resource.lookup', 'ResourceService')
    ->withData(['resource_id' => $id]);

return self::create(
    ErrorCode::RESOURCE_NOT_FOUND,
    "Resource with ID '{$id}' not found",
    $context
);
```

## Benefits

✅ **Type Safety**: Catch specific exceptions, not generic RuntimeException
✅ **Clean API**: `throw ResourceNotFoundException::byId($id)` vs manual setup
✅ **Rich Context**: Automatic operation, component, and data tracking
✅ **Automatic HTTP Status**: ErrorCode category determines response status
✅ **Structured Logging**: Severity-based logging with rich context
✅ **Retry Strategies**: Automatic Retry-After headers for recoverable errors
✅ **Recovery Hints**: Actionable hints for developers in debug mode
✅ **Category-Based Handling**: Catch exceptions by error category
✅ **Testability**: Easy to mock and test specific exception scenarios
✅ **Documentation**: Self-documenting exception hierarchy

## Real-World Example: Queue Module

The Queue module demonstrates the complete pattern:

```php
// Base exception
class QueueException extends FrameworkException { }

// Specific exceptions
final class JobNotFoundException extends QueueException
{
    public static function byId(JobId $jobId): self { }
    public static function inQueue(JobId $jobId, string $queueName): self { }
}

final class ChainNotFoundException extends QueueException
{
    public static function byId(string $chainId): self { }
    public static function byName(string $name): self { }
}

final class InvalidChainStateException extends QueueException
{
    public static function notPending(string $chainId, string $currentStatus): self { }
    public static function alreadyCompleted(string $chainId): self { }
    public static function alreadyFailed(string $chainId): self { }
}

// Usage
throw JobNotFoundException::byId($jobId);
throw ChainNotFoundException::byName($chainName);
throw InvalidChainStateException::notPending($chainId, $status);
```

## See Also

- [ErrorHandler Enhancements Guide](./error-handler-enhancements.md)
- [Developer Migration Guide](./exception-migration-guide.md)
- [ErrorCode Reference](./error-code-reference.md)
