fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\ExceptionHandling\Scope\ErrorScope;
|
||||
use App\Framework\Http\IpAddress;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Http\Session\SessionId;
|
||||
use App\Framework\UserAgent\UserAgent;
|
||||
|
||||
/**
|
||||
* Exception Context Builder
|
||||
*
|
||||
* Automatically collects context from various sources:
|
||||
* - ErrorScope (if available)
|
||||
* - HTTP Request (if in HTTP context)
|
||||
* - Session (if available)
|
||||
* - Authentication (User ID)
|
||||
* - System information (optional)
|
||||
*
|
||||
* Provides a simple API for automatic context collection and reduces boilerplate.
|
||||
*/
|
||||
final readonly class ExceptionContextBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private ?ErrorScope $errorScope = null,
|
||||
private ?ExceptionContextCache $contextCache = null
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build context from current environment
|
||||
*
|
||||
* Automatically collects context from ErrorScope, HTTP Request, Session, etc.
|
||||
*/
|
||||
public function build(?ExceptionContextData $baseContext = null): ExceptionContextData
|
||||
{
|
||||
$context = $baseContext ?? ExceptionContextData::empty();
|
||||
|
||||
// First, enrich from ErrorScope if available
|
||||
if ($this->errorScope !== null) {
|
||||
$scopeContext = $this->errorScope->current();
|
||||
if ($scopeContext !== null) {
|
||||
$context = $this->enrichFromScope($context, $scopeContext);
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build context from HTTP request
|
||||
*
|
||||
* Extracts context from HTTP request, session, and authentication.
|
||||
* Uses cache if available to improve performance.
|
||||
*/
|
||||
public function buildFromRequest(Request $request, ?ExceptionContextData $baseContext = null): ExceptionContextData
|
||||
{
|
||||
// Try to get from cache first
|
||||
if ($this->contextCache !== null) {
|
||||
$requestId = $request instanceof \App\Framework\Http\HttpRequest ? $request->id->toString() : null;
|
||||
$sessionId = property_exists($request, 'session') && $request->session !== null
|
||||
? $request->session->id->toString()
|
||||
: null;
|
||||
$userId = $this->extractUserId($request);
|
||||
|
||||
$cached = $this->contextCache->get($requestId, $sessionId, $userId);
|
||||
if ($cached !== null) {
|
||||
// Merge with base context if provided
|
||||
if ($baseContext !== null) {
|
||||
return $this->mergeContexts($cached, $baseContext);
|
||||
}
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$context = $baseContext ?? ExceptionContextData::empty();
|
||||
|
||||
// Extract request ID
|
||||
if ($request instanceof \App\Framework\Http\HttpRequest) {
|
||||
$context = $context->withRequestId($request->id->toString());
|
||||
}
|
||||
|
||||
// Extract IP address
|
||||
$ipAddress = $request->server->getRemoteAddr();
|
||||
if ($ipAddress !== null) {
|
||||
$ipValue = $ipAddress->value;
|
||||
if (is_string($ipValue) && IpAddress::isValid($ipValue)) {
|
||||
$context = $context->withClientIp(IpAddress::from($ipValue));
|
||||
} else {
|
||||
$context = $context->withClientIp($ipValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract user agent
|
||||
$userAgent = $request->server->getUserAgent();
|
||||
if ($userAgent !== null) {
|
||||
$userAgentValue = $userAgent->value;
|
||||
if (is_string($userAgentValue)) {
|
||||
$context = $context->withUserAgent(UserAgent::fromString($userAgentValue));
|
||||
} else {
|
||||
$context = $context->withUserAgent($userAgentValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract session ID
|
||||
if (property_exists($request, 'session') && $request->session !== null) {
|
||||
$sessionId = $request->session->id->toString();
|
||||
try {
|
||||
$context = $context->withSessionId(SessionId::fromString($sessionId));
|
||||
} catch (\InvalidArgumentException) {
|
||||
// If SessionId validation fails, keep as string for backward compatibility
|
||||
$context = $context->withSessionId($sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract user ID (if authenticated)
|
||||
$userId = $this->extractUserId($request);
|
||||
if ($userId !== null) {
|
||||
$context = $context->withUserId($userId);
|
||||
}
|
||||
|
||||
// Add HTTP-specific tags
|
||||
$context = $context->withTags('http', 'web');
|
||||
|
||||
// Enrich from ErrorScope if available (may override some values)
|
||||
if ($this->errorScope !== null) {
|
||||
$scopeContext = $this->errorScope->current();
|
||||
if ($scopeContext !== null) {
|
||||
$context = $this->enrichFromScope($context, $scopeContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache context if cache is available
|
||||
if ($this->contextCache !== null) {
|
||||
$requestId = $request instanceof \App\Framework\Http\HttpRequest ? $request->id->toString() : null;
|
||||
$sessionId = property_exists($request, 'session') && $request->session !== null
|
||||
? $request->session->id->toString()
|
||||
: null;
|
||||
$userId = $this->extractUserId($request);
|
||||
|
||||
$this->contextCache->put($context, $requestId, $sessionId, $userId);
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two contexts (base takes precedence)
|
||||
*/
|
||||
private function mergeContexts(ExceptionContextData $cached, ExceptionContextData $base): ExceptionContextData
|
||||
{
|
||||
$merged = $cached;
|
||||
|
||||
// Override with base context values if present
|
||||
if ($base->operation !== null) {
|
||||
$merged = $merged->withOperation($base->operation, $base->component);
|
||||
}
|
||||
if ($base->component !== null && $merged->component === null) {
|
||||
$merged = $merged->withOperation($merged->operation ?? '', $base->component);
|
||||
}
|
||||
if (!empty($base->data)) {
|
||||
$merged = $merged->addData($base->data);
|
||||
}
|
||||
if (!empty($base->debug)) {
|
||||
$merged = $merged->addDebug($base->debug);
|
||||
}
|
||||
if (!empty($base->metadata)) {
|
||||
$merged = $merged->addMetadata($base->metadata);
|
||||
}
|
||||
if ($base->userId !== null) {
|
||||
$merged = $merged->withUserId($base->userId);
|
||||
}
|
||||
if ($base->requestId !== null) {
|
||||
$merged = $merged->withRequestId($base->requestId);
|
||||
}
|
||||
if ($base->sessionId !== null) {
|
||||
$merged = $merged->withSessionId($base->sessionId);
|
||||
}
|
||||
if ($base->clientIp !== null) {
|
||||
$merged = $merged->withClientIp($base->clientIp);
|
||||
}
|
||||
if ($base->userAgent !== null) {
|
||||
$merged = $merged->withUserAgent($base->userAgent);
|
||||
}
|
||||
if (!empty($base->tags)) {
|
||||
$merged = $merged->withTags(...array_merge($merged->tags, $base->tags));
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich context from ErrorScope
|
||||
*/
|
||||
private function enrichFromScope(
|
||||
ExceptionContextData $context,
|
||||
\App\Framework\ExceptionHandling\Scope\ErrorScopeContext $scopeContext
|
||||
): ExceptionContextData {
|
||||
// Add operation/component from scope if not already set
|
||||
if ($context->operation === null && $scopeContext->operation !== null) {
|
||||
$context = $context->withOperation(
|
||||
$scopeContext->operation,
|
||||
$scopeContext->component
|
||||
);
|
||||
}
|
||||
|
||||
// Add user ID from scope if not already set
|
||||
if ($context->userId === null && $scopeContext->userId !== null) {
|
||||
$context = $context->withUserId($scopeContext->userId);
|
||||
}
|
||||
|
||||
// Add request ID from scope if not already set
|
||||
if ($context->requestId === null && $scopeContext->requestId !== null) {
|
||||
$context = $context->withRequestId($scopeContext->requestId);
|
||||
}
|
||||
|
||||
// Add session ID from scope if not already set
|
||||
if ($context->sessionId === null && $scopeContext->sessionId !== null) {
|
||||
if (is_string($scopeContext->sessionId)) {
|
||||
try {
|
||||
$context = $context->withSessionId(SessionId::fromString($scopeContext->sessionId));
|
||||
} catch (\InvalidArgumentException) {
|
||||
$context = $context->withSessionId($scopeContext->sessionId);
|
||||
}
|
||||
} else {
|
||||
$context = $context->withSessionId($scopeContext->sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract HTTP fields from scope metadata (for HTTP scopes)
|
||||
if (isset($scopeContext->metadata['ip']) && $context->clientIp === null) {
|
||||
$ipValue = $scopeContext->metadata['ip'];
|
||||
if (is_string($ipValue) && IpAddress::isValid($ipValue)) {
|
||||
$context = $context->withClientIp(IpAddress::from($ipValue));
|
||||
} elseif ($ipValue instanceof IpAddress) {
|
||||
$context = $context->withClientIp($ipValue);
|
||||
} else {
|
||||
$context = $context->withClientIp($ipValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($scopeContext->metadata['user_agent']) && $context->userAgent === null) {
|
||||
$userAgentValue = $scopeContext->metadata['user_agent'];
|
||||
if (is_string($userAgentValue)) {
|
||||
$context = $context->withUserAgent(UserAgent::fromString($userAgentValue));
|
||||
} elseif ($userAgentValue instanceof UserAgent) {
|
||||
$context = $context->withUserAgent($userAgentValue);
|
||||
} else {
|
||||
$context = $context->withUserAgent($userAgentValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Add scope metadata
|
||||
$context = $context->addMetadata([
|
||||
'scope_type' => $scopeContext->type->value,
|
||||
'scope_id' => $scopeContext->scopeId,
|
||||
]);
|
||||
|
||||
// Add scope tags
|
||||
if (!empty($scopeContext->tags)) {
|
||||
$context = $context->withTags(...$scopeContext->tags);
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract user ID from request
|
||||
*/
|
||||
private function extractUserId(Request $request): ?string
|
||||
{
|
||||
// Try to get from request attribute (set by auth middleware)
|
||||
if (method_exists($request, 'getAttribute')) {
|
||||
$user = $request->getAttribute('user');
|
||||
if ($user !== null) {
|
||||
if (is_object($user) && property_exists($user, 'id')) {
|
||||
return (string) $user->id;
|
||||
}
|
||||
if (is_string($user)) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get from request property (if available)
|
||||
if (property_exists($request, 'user') && $request->user !== null) {
|
||||
if (is_object($request->user) && property_exists($request->user, 'id')) {
|
||||
return (string) $request->user->id;
|
||||
}
|
||||
if (is_string($request->user)) {
|
||||
return $request->user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use App\Framework\Cache\Cache;
|
||||
use App\Framework\Cache\CacheItem;
|
||||
use App\Framework\Cache\CacheKey;
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
|
||||
/**
|
||||
* Exception Context Cache
|
||||
*
|
||||
* Caches frequently used context data to improve performance.
|
||||
* Uses cache keys based on Request-ID, Session-ID, User-ID.
|
||||
*/
|
||||
final readonly class ExceptionContextCache
|
||||
{
|
||||
private const string CACHE_PREFIX = 'exception_context:';
|
||||
private const int REQUEST_CONTEXT_TTL = 600; // 10 minutes
|
||||
private const int USER_CONTEXT_TTL = 1800; // 30 minutes
|
||||
|
||||
public function __construct(
|
||||
private Cache $cache
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached context for request
|
||||
*
|
||||
* @param string|null $requestId Request ID
|
||||
* @param string|null $sessionId Session ID
|
||||
* @param string|null $userId User ID
|
||||
* @return ExceptionContextData|null Cached context or null if not found
|
||||
*/
|
||||
public function get(
|
||||
?string $requestId = null,
|
||||
?string $sessionId = null,
|
||||
?string $userId = null
|
||||
): ?ExceptionContextData {
|
||||
// Try request-level cache first (most specific)
|
||||
if ($requestId !== null) {
|
||||
$cacheKey = $this->buildRequestCacheKey($requestId);
|
||||
$cached = $this->getFromCache($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Try session-level cache
|
||||
if ($sessionId !== null) {
|
||||
$cacheKey = $this->buildSessionCacheKey($sessionId);
|
||||
$cached = $this->getFromCache($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Try user-level cache (least specific)
|
||||
if ($userId !== null) {
|
||||
$cacheKey = $this->buildUserCacheKey($userId);
|
||||
$cached = $this->getFromCache($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache context for request
|
||||
*
|
||||
* @param ExceptionContextData $context Context to cache
|
||||
* @param string|null $requestId Request ID
|
||||
* @param string|null $sessionId Session ID
|
||||
* @param string|null $userId User ID
|
||||
*/
|
||||
public function put(
|
||||
ExceptionContextData $context,
|
||||
?string $requestId = null,
|
||||
?string $sessionId = null,
|
||||
?string $userId = null
|
||||
): void {
|
||||
// Cache at request level (most specific)
|
||||
if ($requestId !== null) {
|
||||
$cacheKey = $this->buildRequestCacheKey($requestId);
|
||||
$this->putToCache($cacheKey, $context, Duration::fromSeconds(self::REQUEST_CONTEXT_TTL));
|
||||
}
|
||||
|
||||
// Cache at session level
|
||||
if ($sessionId !== null) {
|
||||
$cacheKey = $this->buildSessionCacheKey($sessionId);
|
||||
$this->putToCache($cacheKey, $context, Duration::fromSeconds(self::REQUEST_CONTEXT_TTL));
|
||||
}
|
||||
|
||||
// Cache at user level (least specific, longer TTL)
|
||||
if ($userId !== null) {
|
||||
$cacheKey = $this->buildUserCacheKey($userId);
|
||||
// Only cache user-specific parts to avoid stale data
|
||||
$userContext = ExceptionContextData::empty()
|
||||
->withUserId($context->userId)
|
||||
->withClientIp($context->clientIp)
|
||||
->withUserAgent($context->userAgent);
|
||||
$this->putToCache($cacheKey, $userContext, Duration::fromSeconds(self::USER_CONTEXT_TTL));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for request
|
||||
*
|
||||
* Called when request context changes (e.g., user logs in/out).
|
||||
*/
|
||||
public function invalidateRequest(?string $requestId = null): void
|
||||
{
|
||||
if ($requestId !== null) {
|
||||
$cacheKey = $this->buildRequestCacheKey($requestId);
|
||||
$this->cache->forget($cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for session
|
||||
*
|
||||
* Called when session changes (e.g., session regenerated).
|
||||
*/
|
||||
public function invalidateSession(?string $sessionId = null): void
|
||||
{
|
||||
if ($sessionId !== null) {
|
||||
$cacheKey = $this->buildSessionCacheKey($sessionId);
|
||||
$this->cache->forget($cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache for user
|
||||
*
|
||||
* Called when user context changes (e.g., user logs in/out).
|
||||
*/
|
||||
public function invalidateUser(?string $userId = null): void
|
||||
{
|
||||
if ($userId !== null) {
|
||||
$cacheKey = $this->buildUserCacheKey($userId);
|
||||
$this->cache->forget($cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key for request
|
||||
*/
|
||||
private function buildRequestCacheKey(string $requestId): CacheKey
|
||||
{
|
||||
return CacheKey::fromString(self::CACHE_PREFIX . 'request:' . $requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key for session
|
||||
*/
|
||||
private function buildSessionCacheKey(string $sessionId): CacheKey
|
||||
{
|
||||
return CacheKey::fromString(self::CACHE_PREFIX . 'session:' . $sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key for user
|
||||
*/
|
||||
private function buildUserCacheKey(string $userId): CacheKey
|
||||
{
|
||||
return CacheKey::fromString(self::CACHE_PREFIX . 'user:' . $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get context from cache
|
||||
*/
|
||||
private function getFromCache(CacheKey $cacheKey): ?ExceptionContextData
|
||||
{
|
||||
$result = $this->cache->get($cacheKey);
|
||||
$item = $result->getFirstHit();
|
||||
|
||||
if ($item === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = $item->value;
|
||||
if ($value instanceof ExceptionContextData) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Try to deserialize from array
|
||||
if (is_array($value)) {
|
||||
return ExceptionContextData::fromArray($value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put context to cache
|
||||
*/
|
||||
private function putToCache(CacheKey $cacheKey, ExceptionContextData $context, Duration $ttl): void
|
||||
{
|
||||
// Store as array for serialization compatibility
|
||||
$cacheItem = CacheItem::fromKey(
|
||||
$cacheKey,
|
||||
$context->toArray(),
|
||||
$ttl
|
||||
);
|
||||
|
||||
$this->cache->set($cacheItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\ExceptionHandling\Context;
|
||||
|
||||
use App\Framework\Http\IpAddress;
|
||||
use App\Framework\Http\RequestId;
|
||||
use App\Framework\Http\Session\SessionId;
|
||||
use App\Framework\UserAgent\UserAgent;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
@@ -26,11 +30,13 @@ final readonly class ExceptionContextData
|
||||
* @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 RequestId|string|null $requestId Request ID for tracing (Value Object or string for backward compatibility)
|
||||
* @param SessionId|string|null $sessionId Session ID if available (Value Object or string for backward compatibility)
|
||||
* @param IpAddress|string|null $clientIp Client IP address for HTTP requests (Value Object or string for backward compatibility)
|
||||
* @param UserAgent|string|null $userAgent User agent string for HTTP requests (Value Object or string for backward compatibility)
|
||||
* @param array<string> $tags Tags for categorization (e.g., ['payment', 'external_api'])
|
||||
* @param bool $auditable Whether this exception should be logged to audit system
|
||||
* @param string|null $auditLevel Audit level for this exception (e.g., 'ERROR', 'WARNING', 'INFO')
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $operation = null,
|
||||
@@ -40,11 +46,13 @@ final readonly class ExceptionContextData
|
||||
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 RequestId|string|null $requestId = null,
|
||||
public SessionId|string|null $sessionId = null,
|
||||
public IpAddress|string|null $clientIp = null,
|
||||
public UserAgent|string|null $userAgent = null,
|
||||
public array $tags = [],
|
||||
public bool $auditable = true,
|
||||
public ?string $auditLevel = null,
|
||||
) {
|
||||
$this->occurredAt ??= new DateTimeImmutable();
|
||||
}
|
||||
@@ -93,7 +101,9 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +124,9 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,7 +147,9 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,7 +170,9 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,14 +193,18 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add request ID
|
||||
*
|
||||
* @param RequestId|string $requestId Request ID (Value Object or string for backward compatibility)
|
||||
*/
|
||||
public function withRequestId(string $requestId): self
|
||||
public function withRequestId(RequestId|string $requestId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
@@ -198,14 +218,18 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add session ID
|
||||
*
|
||||
* @param SessionId|string $sessionId Session ID (Value Object or string for backward compatibility)
|
||||
*/
|
||||
public function withSessionId(string $sessionId): self
|
||||
public function withSessionId(SessionId|string $sessionId): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
@@ -219,14 +243,18 @@ final readonly class ExceptionContextData
|
||||
sessionId: $sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add client IP
|
||||
*
|
||||
* @param IpAddress|string $clientIp Client IP (Value Object or string for backward compatibility)
|
||||
*/
|
||||
public function withClientIp(string $clientIp): self
|
||||
public function withClientIp(IpAddress|string $clientIp): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
@@ -240,14 +268,18 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user agent
|
||||
*
|
||||
* @param UserAgent|string $userAgent User agent (Value Object or string for backward compatibility)
|
||||
*/
|
||||
public function withUserAgent(string $userAgent): self
|
||||
public function withUserAgent(UserAgent|string $userAgent): self
|
||||
{
|
||||
return new self(
|
||||
operation: $this->operation,
|
||||
@@ -261,7 +293,9 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $userAgent,
|
||||
tags: $this->tags
|
||||
tags: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,7 +316,55 @@ final readonly class ExceptionContextData
|
||||
sessionId: $this->sessionId,
|
||||
clientIp: $this->clientIp,
|
||||
userAgent: $this->userAgent,
|
||||
tags: array_merge($this->tags, $tags)
|
||||
tags: array_merge($this->tags, $tags),
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set auditable flag
|
||||
*/
|
||||
public function withAuditable(bool $auditable): 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: $this->tags,
|
||||
auditable: $auditable,
|
||||
auditLevel: $this->auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set audit level
|
||||
*/
|
||||
public function withAuditLevel(string $auditLevel): 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: $this->tags,
|
||||
auditable: $this->auditable,
|
||||
auditLevel: $auditLevel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -299,11 +381,47 @@ final readonly class ExceptionContextData
|
||||
'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,
|
||||
'request_id' => $this->requestId instanceof RequestId ? $this->requestId->toString() : $this->requestId,
|
||||
'session_id' => $this->sessionId instanceof SessionId ? $this->sessionId->toString() : $this->sessionId,
|
||||
'client_ip' => $this->clientIp instanceof IpAddress ? $this->clientIp->value : $this->clientIp,
|
||||
'user_agent' => $this->userAgent instanceof UserAgent ? $this->userAgent->value : $this->userAgent,
|
||||
'tags' => $this->tags,
|
||||
'auditable' => $this->auditable,
|
||||
'audit_level' => $this->auditLevel,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from array (deserialization)
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$occurredAt = null;
|
||||
if (isset($data['occurred_at']) && is_string($data['occurred_at'])) {
|
||||
try {
|
||||
$occurredAt = new DateTimeImmutable($data['occurred_at']);
|
||||
} catch (\Exception) {
|
||||
// Ignore invalid date
|
||||
}
|
||||
}
|
||||
|
||||
return new self(
|
||||
operation: $data['operation'] ?? null,
|
||||
component: $data['component'] ?? null,
|
||||
data: $data['data'] ?? [],
|
||||
debug: $data['debug'] ?? [],
|
||||
metadata: $data['metadata'] ?? [],
|
||||
occurredAt: $occurredAt,
|
||||
userId: $data['user_id'] ?? null,
|
||||
requestId: $data['request_id'] ?? null,
|
||||
sessionId: $data['session_id'] ?? null,
|
||||
clientIp: $data['client_ip'] ?? null,
|
||||
userAgent: $data['user_agent'] ?? null,
|
||||
tags: $data['tags'] ?? [],
|
||||
auditable: $data['auditable'] ?? true,
|
||||
auditLevel: $data['audit_level'] ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,29 +13,19 @@ use WeakMap;
|
||||
* Context is automatically cleaned up when the exception is garbage collected.
|
||||
*
|
||||
* PHP 8.5+ WeakMap-based implementation - no memory leaks possible.
|
||||
*
|
||||
* Should be registered as singleton in DI container for consistent context across application.
|
||||
*/
|
||||
final class ExceptionContextProvider
|
||||
{
|
||||
/** @var WeakMap<\Throwable, ExceptionContextData> */
|
||||
private WeakMap $contexts;
|
||||
|
||||
private static ?self $instance = null;
|
||||
|
||||
private function __construct()
|
||||
public 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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user