refactor(deployment): Remove WireGuard VPN dependency and restore public service access
Remove WireGuard integration from production deployment to simplify infrastructure: - Remove docker-compose-direct-access.yml (VPN-bound services) - Remove VPN-only middlewares from Grafana, Prometheus, Portainer - Remove WireGuard middleware definitions from Traefik - Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers All monitoring services now publicly accessible via subdomains: - grafana.michaelschiemer.de (with Grafana native auth) - prometheus.michaelschiemer.de (with Basic Auth) - portainer.michaelschiemer.de (with Portainer native auth) All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
309
src/Framework/ExceptionHandling/Context/ExceptionContextData.php
Normal file
309
src/Framework/ExceptionHandling/Context/ExceptionContextData.php
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Exception Context Data
|
||||
*
|
||||
* Immutable value object containing rich exception context.
|
||||
* Stored externally via ExceptionContextProvider - never embedded in exceptions.
|
||||
*
|
||||
* PHP 8.5+ readonly class with asymmetric visibility for extensibility.
|
||||
*/
|
||||
final readonly class ExceptionContextData
|
||||
{
|
||||
public readonly DateTimeImmutable $occurredAt;
|
||||
|
||||
/**
|
||||
* @param string|null $operation Operation being performed (e.g., 'user.create', 'payment.process')
|
||||
* @param string|null $component Component where error occurred (e.g., 'UserService', 'PaymentGateway')
|
||||
* @param array<string, mixed> $data Domain data (e.g., user_id, order_id, amount)
|
||||
* @param array<string, mixed> $debug Debug data (queries, traces, internal state)
|
||||
* @param array<string, mixed> $metadata Additional metadata (tags, severity, fingerprint)
|
||||
* @param DateTimeImmutable|null $occurredAt When the exception occurred
|
||||
* @param string|null $userId User ID if available
|
||||
* @param string|null $requestId Request ID for tracing
|
||||
* @param string|null $sessionId Session ID if available
|
||||
* @param string|null $clientIp Client IP address for HTTP requests
|
||||
* @param string|null $userAgent User agent string for HTTP requests
|
||||
* @param array<string> $tags Tags for categorization (e.g., ['payment', 'external_api'])
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $operation = null,
|
||||
public ?string $component = null,
|
||||
public array $data = [],
|
||||
public array $debug = [],
|
||||
public array $metadata = [],
|
||||
?DateTimeImmutable $occurredAt = null,
|
||||
public ?string $userId = null,
|
||||
public ?string $requestId = null,
|
||||
public ?string $sessionId = null,
|
||||
public ?string $clientIp = null,
|
||||
public ?string $userAgent = null,
|
||||
public array $tags = [],
|
||||
) {
|
||||
$this->occurredAt ??= new DateTimeImmutable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create empty context
|
||||
*/
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create context with operation
|
||||
*/
|
||||
public static function forOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
operation: $operation,
|
||||
component: $component
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create context with data
|
||||
*/
|
||||
public static function withData(array $data): self
|
||||
{
|
||||
return new self(data: $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new instance with operation
|
||||
*/
|
||||
public function withOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
operation: $operation,
|
||||
component: $component ?? $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to context
|
||||
*/
|
||||
public function addData(array $data): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: array_merge($this->data, $data),
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add debug information
|
||||
*/
|
||||
public function addDebug(array $debug): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: array_merge($this->debug, $debug),
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata
|
||||
*/
|
||||
public function addMetadata(array $metadata): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: array_merge($this->metadata, $metadata),
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user ID
|
||||
*/
|
||||
public function withUserId(string $userId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add request ID
|
||||
*/
|
||||
public function withRequestId(string $requestId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add session ID
|
||||
*/
|
||||
public function withSessionId(string $sessionId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add client IP
|
||||
*/
|
||||
public function withClientIp(string $clientIp): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user agent
|
||||
*/
|
||||
public function withUserAgent(string $userAgent): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $userAgent,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags
|
||||
*/
|
||||
public function withTags(string ...$tags): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
data: $this->data,
|
||||
debug: $this->debug,
|
||||
metadata: $this->metadata,
|
||||
occurredAt: $this->occurredAt,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: array_merge($this->tags, $tags)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array for serialization
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'operation' => $this->operation,
|
||||
'component' => $this->component,
|
||||
'data' => $this->data,
|
||||
'debug' => $this->debug,
|
||||
'metadata' => $this->metadata,
|
||||
'occurred_at' => $this->occurredAt?->format('Y-m-d H:i:s.u'),
|
||||
'user_id' => $this->userId,
|
||||
'request_id' => $this->requestId,
|
||||
'session_id' => $this->sessionId,
|
||||
'client_ip' => $this->clientIp,
|
||||
'user_agent' => $this->userAgent,
|
||||
'tags' => $this->tags,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use WeakMap;
|
||||
|
||||
/**
|
||||
* Exception Context Provider
|
||||
*
|
||||
* Manages exception context externally using WeakMap for automatic garbage collection.
|
||||
* Context is automatically cleaned up when the exception is garbage collected.
|
||||
*
|
||||
* PHP 8.5+ WeakMap-based implementation - no memory leaks possible.
|
||||
*/
|
||||
final class ExceptionContextProvider
|
||||
{
|
||||
/** @var WeakMap<\Throwable, ExceptionContextData> */
|
||||
private WeakMap $contexts;
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$this->contexts = new WeakMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
*
|
||||
* Singleton pattern ensures consistent context across the application
|
||||
*/
|
||||
public static function instance(): self
|
||||
{
|
||||
return self::$instance ??= new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach context to exception
|
||||
*
|
||||
* @param \Throwable $exception The exception to attach context to
|
||||
* @param ExceptionContextData $context The context data
|
||||
*/
|
||||
public function attach(\Throwable $exception, ExceptionContextData $context): void
|
||||
{
|
||||
$this->contexts[$exception] = $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context for exception
|
||||
*
|
||||
* @param \Throwable $exception The exception to get context for
|
||||
* @return ExceptionContextData|null The context data or null if not found
|
||||
*/
|
||||
public function get(\Throwable $exception): ?ExceptionContextData
|
||||
{
|
||||
return $this->contexts[$exception] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if exception has context
|
||||
*
|
||||
* @param \Throwable $exception The exception to check
|
||||
* @return bool True if context exists
|
||||
*/
|
||||
public function has(\Throwable $exception): bool
|
||||
{
|
||||
return isset($this->contexts[$exception]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove context from exception
|
||||
*
|
||||
* Note: Usually not needed due to WeakMap automatic cleanup,
|
||||
* but provided for explicit control if needed.
|
||||
*
|
||||
* @param \Throwable $exception The exception to remove context from
|
||||
*/
|
||||
public function detach(\Throwable $exception): void
|
||||
{
|
||||
unset($this->contexts[$exception]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about context storage
|
||||
*
|
||||
* @return array{total_contexts: int}
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
// WeakMap doesn't provide count(), so we iterate
|
||||
$count = 0;
|
||||
foreach ($this->contexts as $_) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
return [
|
||||
'total_contexts' => $count,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all contexts
|
||||
*
|
||||
* Mainly for testing purposes
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->contexts = new WeakMap();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\ExceptionHandling\Renderers\ResponseErrorRenderer;
|
||||
use App\Framework\ExceptionHandling\Reporter\LogReporter;
|
||||
use App\Framework\Http\Response;
|
||||
use Throwable;
|
||||
|
||||
final readonly class ErrorKernel
|
||||
@@ -25,5 +28,28 @@ final readonly class ErrorKernel
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTTP Response from exception without terminating execution
|
||||
*
|
||||
* This method enables middleware recovery patterns by returning a Response
|
||||
* object instead of terminating the application.
|
||||
*
|
||||
* @param Throwable $exception Exception to render
|
||||
* @param ExceptionContextProvider|null $contextProvider Optional WeakMap context provider
|
||||
* @param bool $isDebugMode Enable debug information in response
|
||||
* @return Response HTTP Response object (JSON for API, HTML for web)
|
||||
*/
|
||||
public function createHttpResponse(
|
||||
Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null,
|
||||
bool $isDebugMode = false
|
||||
): Response {
|
||||
// Create ResponseErrorRenderer with debug mode setting
|
||||
$renderer = new ResponseErrorRenderer($isDebugMode);
|
||||
|
||||
// Generate and return Response object
|
||||
return $renderer->createResponse($exception, $contextProvider);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling;
|
||||
|
||||
use App\Framework\DI\Initializer;
|
||||
use Fiber;
|
||||
|
||||
final class ErrorScope
|
||||
{
|
||||
private array $stack = [];
|
||||
|
||||
#[Initializer]
|
||||
public static function initialize(): ErrorScope
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
public function enter(ErrorScopeContext $context): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$this->stack[$id] ??= [];
|
||||
$this->stack[$id][] = $context;
|
||||
return count($this->stack[$id]);
|
||||
}
|
||||
|
||||
public function current(): ?ErrorScopeContext
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$stack = $this->stack[$id] ?? [];
|
||||
return end($stack) ?? null;
|
||||
}
|
||||
|
||||
public function leave(int $token): void
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
if(!isset($this->stack[$id])) {
|
||||
return;
|
||||
}
|
||||
while(!empty($this->stack[$id]) && count($this->stack[$id]) >= $token) {
|
||||
array_pop($this->stack[$id]);
|
||||
}
|
||||
if(empty($this->stack[$id])) {
|
||||
unset($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
private function fiberId(): int
|
||||
{
|
||||
$fiber = Fiber::getCurrent();
|
||||
return $fiber ? spl_object_id($fiber) : 0;
|
||||
}
|
||||
}
|
||||
200
src/Framework/ExceptionHandling/Factory/ExceptionFactory.php
Normal file
200
src/Framework/ExceptionHandling/Factory/ExceptionFactory.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Factory;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextData;
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\ExceptionHandling\Scope\ErrorScope;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Exception Factory
|
||||
*
|
||||
* Helper for creating exceptions with external context.
|
||||
* Integrates with ErrorScope for automatic context enrichment.
|
||||
*
|
||||
* PHP 8.5+ with WeakMap-based context management.
|
||||
*/
|
||||
final readonly class ExceptionFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ExceptionContextProvider $contextProvider,
|
||||
private ErrorScope $errorScope
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create exception with context
|
||||
*
|
||||
* @template T of Throwable
|
||||
* @param class-string<T> $exceptionClass
|
||||
* @param string $message
|
||||
* @param ExceptionContextData|null $context
|
||||
* @param Throwable|null $previous
|
||||
* @return T
|
||||
*/
|
||||
public function create(
|
||||
string $exceptionClass,
|
||||
string $message,
|
||||
?ExceptionContextData $context = null,
|
||||
?\Throwable $previous = null
|
||||
): Throwable {
|
||||
// Create slim exception (pure PHP)
|
||||
$exception = new $exceptionClass($message, 0, $previous);
|
||||
|
||||
// Enrich context from current scope
|
||||
$enrichedContext = $this->enrichContext($context);
|
||||
|
||||
// Attach context externally via WeakMap
|
||||
$this->contextProvider->attach($exception, $enrichedContext);
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance existing exception with context
|
||||
*
|
||||
* Useful for rethrowing exceptions with additional context
|
||||
*
|
||||
* @param Throwable $exception
|
||||
* @param ExceptionContextData $additionalContext
|
||||
* @return Throwable
|
||||
*/
|
||||
public function enhance(
|
||||
Throwable $exception,
|
||||
ExceptionContextData $additionalContext
|
||||
): Throwable {
|
||||
// Get existing context if any
|
||||
$existingContext = $this->contextProvider->get($exception);
|
||||
|
||||
// Merge contexts
|
||||
$mergedContext = $existingContext
|
||||
? $existingContext
|
||||
->addData($additionalContext->data)
|
||||
->addDebug($additionalContext->debug)
|
||||
->addMetadata($additionalContext->metadata)
|
||||
: $additionalContext;
|
||||
|
||||
// Enrich from scope
|
||||
$enrichedContext = $this->enrichContext($mergedContext);
|
||||
|
||||
// Update context
|
||||
$this->contextProvider->attach($exception, $enrichedContext);
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception with operation context
|
||||
*
|
||||
* Convenience method for common use case
|
||||
*
|
||||
* @template T of Throwable
|
||||
* @param class-string<T> $exceptionClass
|
||||
* @param string $message
|
||||
* @param string $operation
|
||||
* @param string|null $component
|
||||
* @param array<string, mixed> $data
|
||||
* @param Throwable|null $previous
|
||||
* @return T
|
||||
*/
|
||||
public function forOperation(
|
||||
string $exceptionClass,
|
||||
string $message,
|
||||
string $operation,
|
||||
?string $component = null,
|
||||
array $data = [],
|
||||
?\Throwable $previous = null
|
||||
): Throwable {
|
||||
$context = ExceptionContextData::forOperation($operation, $component)
|
||||
->addData($data);
|
||||
|
||||
return $this->create($exceptionClass, $message, $context, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception with data
|
||||
*
|
||||
* Convenience method for exceptions with data payload
|
||||
*
|
||||
* @template T of Throwable
|
||||
* @param class-string<T> $exceptionClass
|
||||
* @param string $message
|
||||
* @param array<string, mixed> $data
|
||||
* @param Throwable|null $previous
|
||||
* @return T
|
||||
*/
|
||||
public function withData(
|
||||
string $exceptionClass,
|
||||
string $message,
|
||||
array $data,
|
||||
?\Throwable $previous = null
|
||||
): Throwable {
|
||||
$context = ExceptionContextData::withData($data);
|
||||
|
||||
return $this->create($exceptionClass, $message, $context, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich context from current error scope
|
||||
*
|
||||
* @param ExceptionContextData|null $context
|
||||
* @return ExceptionContextData
|
||||
*/
|
||||
private function enrichContext(?ExceptionContextData $context): ExceptionContextData
|
||||
{
|
||||
$scopeContext = $this->errorScope->current();
|
||||
|
||||
if ($scopeContext === null) {
|
||||
return $context ?? ExceptionContextData::empty();
|
||||
}
|
||||
|
||||
// Start with provided context or empty
|
||||
$enriched = $context ?? ExceptionContextData::empty();
|
||||
|
||||
// Enrich with scope data
|
||||
$enriched = $enriched
|
||||
->addMetadata([
|
||||
'scope_type' => $scopeContext->type->value,
|
||||
'scope_id' => $scopeContext->scopeId,
|
||||
]);
|
||||
|
||||
// Add operation/component from scope if not already set
|
||||
if ($enriched->operation === null && $scopeContext->operation !== null) {
|
||||
$enriched = $enriched->withOperation(
|
||||
$scopeContext->operation,
|
||||
$scopeContext->component
|
||||
);
|
||||
}
|
||||
|
||||
// Add user/request/session IDs from scope
|
||||
if ($scopeContext->userId !== null) {
|
||||
$enriched = $enriched->withUserId($scopeContext->userId);
|
||||
}
|
||||
|
||||
if ($scopeContext->requestId !== null) {
|
||||
$enriched = $enriched->withRequestId($scopeContext->requestId);
|
||||
}
|
||||
|
||||
if ($scopeContext->sessionId !== null) {
|
||||
$enriched = $enriched->withSessionId($scopeContext->sessionId);
|
||||
}
|
||||
|
||||
// Extract HTTP fields from scope metadata (for HTTP scopes)
|
||||
if (isset($scopeContext->metadata['ip'])) {
|
||||
$enriched = $enriched->withClientIp($scopeContext->metadata['ip']);
|
||||
}
|
||||
|
||||
if (isset($scopeContext->metadata['user_agent'])) {
|
||||
$enriched = $enriched->withUserAgent($scopeContext->metadata['user_agent']);
|
||||
}
|
||||
|
||||
// Add scope tags
|
||||
if (!empty($scopeContext->tags)) {
|
||||
$enriched = $enriched->withTags(...$scopeContext->tags);
|
||||
}
|
||||
|
||||
return $enriched;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Renderers;
|
||||
|
||||
use App\Framework\ExceptionHandling\Context\ExceptionContextProvider;
|
||||
use App\Framework\Http\Response;
|
||||
use App\Framework\Http\Status;
|
||||
|
||||
/**
|
||||
* HTTP Response factory for API and HTML error pages
|
||||
*
|
||||
* Extracts Response generation logic from ErrorKernel for reuse
|
||||
* in middleware recovery patterns.
|
||||
*/
|
||||
final readonly class ResponseErrorRenderer
|
||||
{
|
||||
public function __construct(
|
||||
private bool $isDebugMode = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create HTTP Response from exception
|
||||
*
|
||||
* @param \Throwable $exception Exception to render
|
||||
* @param ExceptionContextProvider|null $contextProvider Optional context provider
|
||||
* @return Response HTTP Response object
|
||||
*/
|
||||
public function createResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider = null
|
||||
): Response {
|
||||
// Determine if API or HTML response needed
|
||||
$isApiRequest = $this->isApiRequest();
|
||||
|
||||
if ($isApiRequest) {
|
||||
return $this->createApiResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return $this->createHtmlResponse($exception, $contextProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create JSON API error response
|
||||
*/
|
||||
private function createApiResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$errorData = [
|
||||
'error' => [
|
||||
'message' => $this->isDebugMode
|
||||
? $exception->getMessage()
|
||||
: 'An error occurred while processing your request.',
|
||||
'type' => $this->isDebugMode ? get_class($exception) : 'ServerError',
|
||||
'code' => $exception->getCode(),
|
||||
]
|
||||
];
|
||||
|
||||
// Add debug information if enabled
|
||||
if ($this->isDebugMode) {
|
||||
$errorData['error']['file'] = $exception->getFile();
|
||||
$errorData['error']['line'] = $exception->getLine();
|
||||
$errorData['error']['trace'] = $this->formatStackTrace($exception);
|
||||
|
||||
// Add context from WeakMap if available
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null) {
|
||||
$errorData['context'] = [
|
||||
'operation' => $context->operation,
|
||||
'component' => $context->component,
|
||||
'request_id' => $context->requestId,
|
||||
'occurred_at' => $context->occurredAt?->format('Y-m-d H:i:s'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$body = json_encode($errorData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $body,
|
||||
headers: [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HTML error page response
|
||||
*/
|
||||
private function createHtmlResponse(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): Response {
|
||||
$statusCode = $this->getHttpStatusCode($exception);
|
||||
|
||||
$html = $this->generateErrorHtml(
|
||||
$exception,
|
||||
$contextProvider,
|
||||
$statusCode
|
||||
);
|
||||
|
||||
return new Response(
|
||||
status: Status::from($statusCode),
|
||||
body: $html,
|
||||
headers: [
|
||||
'Content-Type' => 'text/html; charset=utf-8',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML error page
|
||||
*/
|
||||
private function generateErrorHtml(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider,
|
||||
int $statusCode
|
||||
): string {
|
||||
$title = $this->getErrorTitle($statusCode);
|
||||
$message = $this->isDebugMode
|
||||
? $exception->getMessage()
|
||||
: 'An error occurred while processing your request.';
|
||||
|
||||
$debugInfo = '';
|
||||
if ($this->isDebugMode) {
|
||||
$debugInfo = $this->generateDebugSection($exception, $contextProvider);
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{$title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.error-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #d32f2f;
|
||||
margin-top: 0;
|
||||
}
|
||||
.error-message {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.debug-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.debug-info pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.context-item {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.context-label {
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>{$title}</h1>
|
||||
<div class="error-message">
|
||||
<p>{$message}</p>
|
||||
</div>
|
||||
{$debugInfo}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate debug information section
|
||||
*/
|
||||
private function generateDebugSection(
|
||||
\Throwable $exception,
|
||||
?ExceptionContextProvider $contextProvider
|
||||
): string {
|
||||
$exceptionClass = get_class($exception);
|
||||
$file = $exception->getFile();
|
||||
$line = $exception->getLine();
|
||||
$trace = $this->formatStackTrace($exception);
|
||||
|
||||
$contextHtml = '';
|
||||
if ($contextProvider !== null) {
|
||||
$context = $contextProvider->get($exception);
|
||||
if ($context !== null) {
|
||||
$contextHtml = <<<HTML
|
||||
<div class="context-item">
|
||||
<span class="context-label">Operation:</span> {$context->operation}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Component:</span> {$context->component}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Request ID:</span> {$context->requestId}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Occurred At:</span> {$context->occurredAt?->format('Y-m-d H:i:s')}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="debug-info">
|
||||
<h3>Debug Information</h3>
|
||||
<div class="context-item">
|
||||
<span class="context-label">Exception:</span> {$exceptionClass}
|
||||
</div>
|
||||
<div class="context-item">
|
||||
<span class="context-label">File:</span> {$file}:{$line}
|
||||
</div>
|
||||
{$contextHtml}
|
||||
<h4>Stack Trace:</h4>
|
||||
<pre>{$trace}</pre>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if current request is API request
|
||||
*/
|
||||
private function isApiRequest(): bool
|
||||
{
|
||||
// Check for JSON Accept header
|
||||
$acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
if (str_contains($acceptHeader, 'application/json')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for API path prefix
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
if (str_starts_with($requestUri, '/api/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for AJAX requests
|
||||
$requestedWith = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? '';
|
||||
if (strtolower($requestedWith) === 'xmlhttprequest') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTTP status code from exception
|
||||
*/
|
||||
private function getHttpStatusCode(\Throwable $exception): int
|
||||
{
|
||||
// Use exception code if it's a valid HTTP status code
|
||||
$code = $exception->getCode();
|
||||
if ($code >= 400 && $code < 600) {
|
||||
return $code;
|
||||
}
|
||||
|
||||
// Map common exceptions to status codes
|
||||
return match (true) {
|
||||
$exception instanceof \InvalidArgumentException => 400,
|
||||
$exception instanceof \RuntimeException => 500,
|
||||
$exception instanceof \LogicException => 500,
|
||||
default => 500,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error title from status code
|
||||
*/
|
||||
private function getErrorTitle(int $statusCode): string
|
||||
{
|
||||
return match ($statusCode) {
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
429 => 'Too Many Requests',
|
||||
500 => 'Internal Server Error',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
default => "Error {$statusCode}",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format stack trace for display
|
||||
*/
|
||||
private function formatStackTrace(\Throwable $exception): string
|
||||
{
|
||||
$trace = $exception->getTraceAsString();
|
||||
|
||||
// Limit trace depth in production
|
||||
if (!$this->isDebugMode) {
|
||||
$lines = explode("\n", $trace);
|
||||
$trace = implode("\n", array_slice($lines, 0, 5));
|
||||
}
|
||||
|
||||
return htmlspecialchars($trace, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
135
src/Framework/ExceptionHandling/Scope/ErrorScope.php
Normal file
135
src/Framework/ExceptionHandling/Scope/ErrorScope.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Scope;
|
||||
|
||||
use App\Framework\DI\Initializer;
|
||||
use Fiber;
|
||||
|
||||
/**
|
||||
* Error Scope Stack Manager
|
||||
*
|
||||
* Manages fiber-aware scope stack for error context enrichment.
|
||||
* Each fiber has its own isolated scope stack.
|
||||
*
|
||||
* PHP 8.5+ with fiber isolation and automatic cleanup.
|
||||
*/
|
||||
final class ErrorScope
|
||||
{
|
||||
/** @var array<int, array<ErrorScopeContext>> Fiber-specific scope stacks */
|
||||
private array $stack = [];
|
||||
|
||||
#[Initializer]
|
||||
public static function initialize(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter a new error scope
|
||||
*
|
||||
* @param ErrorScopeContext $context Scope context to enter
|
||||
* @return int Token for leaving this scope (stack depth)
|
||||
*/
|
||||
public function enter(ErrorScopeContext $context): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$this->stack[$id] ??= [];
|
||||
$this->stack[$id][] = $context;
|
||||
|
||||
return count($this->stack[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit error scope(s)
|
||||
*
|
||||
* @param int $token Token from enter() - exits all scopes until this depth
|
||||
*/
|
||||
public function exit(int $token = 0): void
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
|
||||
if (!isset($this->stack[$id])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($token === 0) {
|
||||
// Exit only the most recent scope
|
||||
array_pop($this->stack[$id]);
|
||||
} else {
|
||||
// Exit all scopes until token depth
|
||||
while (!empty($this->stack[$id]) && count($this->stack[$id]) >= $token) {
|
||||
array_pop($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup empty stack
|
||||
if (empty($this->stack[$id])) {
|
||||
unset($this->stack[$id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current error scope context
|
||||
*
|
||||
* @return ErrorScopeContext|null Current scope or null if no scope active
|
||||
*/
|
||||
public function current(): ?ErrorScopeContext
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
$stack = $this->stack[$id] ?? [];
|
||||
|
||||
$current = end($stack);
|
||||
return $current !== false ? $current : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any scope is active
|
||||
*/
|
||||
public function hasScope(): bool
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
return !empty($this->stack[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scope depth (number of nested scopes)
|
||||
*/
|
||||
public function depth(): int
|
||||
{
|
||||
$id = $this->fiberId();
|
||||
return count($this->stack[$id] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fiber ID for isolation
|
||||
*
|
||||
* Returns 0 for main fiber, unique ID for each Fiber
|
||||
*/
|
||||
private function fiberId(): int
|
||||
{
|
||||
$fiber = Fiber::getCurrent();
|
||||
return $fiber ? spl_object_id($fiber) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all scopes (for testing/cleanup)
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics for monitoring
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
return [
|
||||
'active_fibers' => count($this->stack),
|
||||
'total_scopes' => array_sum(array_map('count', $this->stack)),
|
||||
'max_depth' => !empty($this->stack) ? max(array_map('count', $this->stack)) : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
276
src/Framework/ExceptionHandling/Scope/ErrorScopeContext.php
Normal file
276
src/Framework/ExceptionHandling/Scope/ErrorScopeContext.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Scope;
|
||||
|
||||
use App\Framework\Http\Request;
|
||||
|
||||
/**
|
||||
* Error Scope Context
|
||||
*
|
||||
* Rich context for error scopes (HTTP, Console, Job, CLI, etc.).
|
||||
* Works with ErrorScope for fiber-aware scope stack management.
|
||||
*
|
||||
* PHP 8.5+ readonly class with factory methods for different scope types.
|
||||
*/
|
||||
final readonly class ErrorScopeContext
|
||||
{
|
||||
/**
|
||||
* @param ErrorScopeType $type Scope type (HTTP, Console, Job, etc.)
|
||||
* @param string $scopeId Unique scope identifier
|
||||
* @param string|null $operation Operation being performed
|
||||
* @param string|null $component Component executing the operation
|
||||
* @param array<string, mixed> $metadata Additional metadata
|
||||
* @param string|null $userId User ID if authenticated
|
||||
* @param string|null $requestId Request ID for HTTP scopes
|
||||
* @param string|null $sessionId Session ID if available
|
||||
* @param string|null $jobId Job ID for background job scopes
|
||||
* @param string|null $commandName Console command name
|
||||
* @param array<string> $tags Tags for categorization
|
||||
*/
|
||||
public function __construct(
|
||||
public ErrorScopeType $type,
|
||||
public string $scopeId,
|
||||
public ?string $operation = null,
|
||||
public ?string $component = null,
|
||||
public array $metadata = [],
|
||||
public ?string $userId = null,
|
||||
public ?string $requestId = null,
|
||||
public ?string $sessionId = null,
|
||||
public ?string $jobId = null,
|
||||
public ?string $commandName = null,
|
||||
public array $tags = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create HTTP scope from request
|
||||
*/
|
||||
public static function http(
|
||||
Request $request,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::HTTP,
|
||||
scopeId: $request->headers->getFirst('X-Request-ID')
|
||||
?? uniqid('http_', true),
|
||||
operation: $operation,
|
||||
component: $component,
|
||||
metadata: [
|
||||
'method' => $request->method->value,
|
||||
'path' => $request->path,
|
||||
'ip' => $request->server->getRemoteAddr(),
|
||||
'user_agent' => $request->server->getUserAgent(),
|
||||
],
|
||||
requestId: $request->headers->getFirst('X-Request-ID'),
|
||||
sessionId: property_exists($request, 'session') ? $request->session?->getId() : null,
|
||||
userId: property_exists($request, 'user') ? ($request->user?->id ?? null) : null,
|
||||
tags: ['http', 'web']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create console scope
|
||||
*/
|
||||
public static function console(
|
||||
string $commandName,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::CONSOLE,
|
||||
scopeId: uniqid('console_', true),
|
||||
operation: $operation ?? "console.{$commandName}",
|
||||
component: $component,
|
||||
metadata: [
|
||||
'command' => $commandName,
|
||||
'argv' => $_SERVER['argv'] ?? [],
|
||||
'cwd' => getcwd(),
|
||||
],
|
||||
commandName: $commandName,
|
||||
tags: ['console', 'cli']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create background job scope
|
||||
*/
|
||||
public static function job(
|
||||
string $jobId,
|
||||
string $jobClass,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::JOB,
|
||||
scopeId: $jobId,
|
||||
operation: $operation ?? "job.{$jobClass}",
|
||||
component: $component ?? $jobClass,
|
||||
metadata: [
|
||||
'job_class' => $jobClass,
|
||||
'job_id' => $jobId,
|
||||
],
|
||||
jobId: $jobId,
|
||||
tags: ['job', 'background', 'async']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CLI scope
|
||||
*/
|
||||
public static function cli(
|
||||
string $script,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::CLI,
|
||||
scopeId: uniqid('cli_', true),
|
||||
operation: $operation ?? "cli.{$script}",
|
||||
component: $component,
|
||||
metadata: [
|
||||
'script' => $script,
|
||||
'argv' => $_SERVER['argv'] ?? [],
|
||||
],
|
||||
tags: ['cli', 'script']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test scope
|
||||
*/
|
||||
public static function test(
|
||||
string $testName,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::TEST,
|
||||
scopeId: uniqid('test_', true),
|
||||
operation: $operation ?? "test.{$testName}",
|
||||
component: $component,
|
||||
metadata: [
|
||||
'test_name' => $testName,
|
||||
],
|
||||
tags: ['test', 'testing']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create generic scope
|
||||
*/
|
||||
public static function generic(
|
||||
string $scopeId,
|
||||
?string $operation = null,
|
||||
?string $component = null
|
||||
): self {
|
||||
return new self(
|
||||
type: ErrorScopeType::GENERIC,
|
||||
scopeId: $scopeId,
|
||||
operation: $operation,
|
||||
component: $component,
|
||||
tags: ['generic']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add operation
|
||||
*/
|
||||
public function withOperation(string $operation, ?string $component = null): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $operation,
|
||||
component: $component ?? $this->component,
|
||||
metadata: $this->metadata,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata
|
||||
*/
|
||||
public function addMetadata(array $metadata): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
metadata: array_merge($this->metadata, $metadata),
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user ID
|
||||
*/
|
||||
public function withUserId(string $userId): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
metadata: $this->metadata,
|
||||
userId: $userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: $this->tags
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags
|
||||
*/
|
||||
public function withTags(string ...$tags): self
|
||||
{
|
||||
return new self(
|
||||
type: $this->type,
|
||||
scopeId: $this->scopeId,
|
||||
operation: $this->operation,
|
||||
component: $this->component,
|
||||
metadata: $this->metadata,
|
||||
userId: $this->userId,
|
||||
requestId: $this->requestId,
|
||||
sessionId: $this->sessionId,
|
||||
jobId: $this->jobId,
|
||||
commandName: $this->commandName,
|
||||
tags: array_merge($this->tags, $tags)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type->value,
|
||||
'scope_id' => $this->scopeId,
|
||||
'operation' => $this->operation,
|
||||
'component' => $this->component,
|
||||
'metadata' => $this->metadata,
|
||||
'user_id' => $this->userId,
|
||||
'request_id' => $this->requestId,
|
||||
'session_id' => $this->sessionId,
|
||||
'job_id' => $this->jobId,
|
||||
'command_name' => $this->commandName,
|
||||
'tags' => $this->tags,
|
||||
];
|
||||
}
|
||||
}
|
||||
55
src/Framework/ExceptionHandling/Scope/ErrorScopeType.php
Normal file
55
src/Framework/ExceptionHandling/Scope/ErrorScopeType.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Scope;
|
||||
|
||||
/**
|
||||
* Error Scope Type
|
||||
*
|
||||
* Defines the different types of error scopes in the application.
|
||||
*/
|
||||
enum ErrorScopeType: string
|
||||
{
|
||||
case HTTP = 'http';
|
||||
case CONSOLE = 'console';
|
||||
case JOB = 'job';
|
||||
case CLI = 'cli';
|
||||
case TEST = 'test';
|
||||
case GENERIC = 'generic';
|
||||
|
||||
/**
|
||||
* Check if scope is web-based
|
||||
*/
|
||||
public function isWeb(): bool
|
||||
{
|
||||
return $this === self::HTTP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scope is CLI-based
|
||||
*/
|
||||
public function isCli(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::CONSOLE, self::CLI => true,
|
||||
default => false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scope is async/background
|
||||
*/
|
||||
public function isAsync(): bool
|
||||
{
|
||||
return $this === self::JOB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scope is for testing
|
||||
*/
|
||||
public function isTest(): bool
|
||||
{
|
||||
return $this === self::TEST;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user