- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
18 KiB
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
- FrameworkException: Base exception with ErrorCode and ExceptionContext
- ModuleException: Module-specific base (e.g.,
QueueException,DatabaseException) - Specific Exceptions: Domain-specific exceptions with factory methods
- ErrorCode Enum: Category-specific error codes (e.g.,
QueueErrorCode,DatabaseErrorCode)
Creating a Module Exception Hierarchy
Step 1: Create Module Base Exception
<?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
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 requiredERROR: Operation failed, attention neededWARNING: Potential issue, monitorINFO: Informational, no action neededDEBUG: Debugging information
Step 3: Create Specific Exceptions
Create domain-specific exceptions with factory methods:
<?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:
// ❌ 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:
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:
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:
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:
$context = ExceptionContext::forOperation(
'order.process', // Operation: what was being attempted
'OrderService' // Component: which class/service
);
Context Data
Add relevant data for debugging:
$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):
$context->withDebug([
'query' => $sql,
'bind_params' => $params,
'execution_time_ms' => $executionTime,
]);
Metadata
Add structured metadata:
$context->withMetadata([
'attempt' => $attemptNumber,
'max_attempts' => $maxAttempts,
'retry_strategy' => 'exponential_backoff',
]);
Exception Catching Patterns
Catch Specific Exception
try {
$resource = $this->resourceService->find($id);
} catch (ResourceNotFoundException $e) {
// Handle specific exception
return $this->notFoundResponse($e->getMessage());
}
Catch Module-Level Exception
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
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
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
// 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
// ErrorCode.getSeverity() determines log level:
// CRITICAL -> critical log
// ERROR -> error log
// WARNING -> warning log
// INFO -> info log
// DEBUG -> debug log
Automatic Retry-After Headers
// ErrorCode.getRetryAfterSeconds() adds Retry-After header:
if ($errorCode->getRetryAfterSeconds() !== null) {
$response->headers['Retry-After'] = $retryAfter;
}
Recovery Hints in Debug Mode
// 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}ErrorCodeenum - Implement all required ErrorCode methods (getValue, getCategory, getSeverity, etc.)
- Create specific exceptions with factory methods
- Replace
RuntimeExceptionwith domain-specific exceptions - Replace
InvalidArgumentExceptionin 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:
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:
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:
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
// ❌ 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
// ❌ 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
// ❌ 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
// ❌ 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:
// 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);