$frame */ public static function fromArray(array $frame): self { // Bereinige Args, um nicht-serialisierbare Objekte zu entfernen $args = isset($frame['args']) ? self::sanitizeArgs($frame['args']) : []; // Normalisiere Klassenname: Forward-Slashes zu Backslashes $class = null; if (isset($frame['class']) && is_string($frame['class'])) { $class = str_replace('/', '\\', $frame['class']); } return new self( file: $frame['file'] ?? 'unknown', line: $frame['line'] ?? 0, function: $frame['function'] ?? null, class: $class, type: $frame['type'] ?? null, args: $args ); } /** * Bereinigt Args, um nicht-serialisierbare Objekte (wie ReflectionClass) zu entfernen * * @param array $args * @return array */ private static function sanitizeArgs(array $args): array { return array_map( fn ($arg) => self::sanitizeValue($arg), $args ); } /** * Bereinigt einzelnen Wert, entfernt nicht-serialisierbare Objekte */ private static function sanitizeValue(mixed $value): mixed { // Closures können nicht serialisiert werden if ($value instanceof \Closure) { try { $reflection = new \ReflectionFunction($value); $file = $reflection->getFileName(); $line = $reflection->getStartLine(); return sprintf('Closure(%s:%d)', basename($file), $line); } catch (\Throwable) { return 'Closure'; } } // Reflection-Objekte können nicht serialisiert werden if (is_object($value)) { $className = get_class($value); if ($value instanceof \ReflectionClass || $value instanceof \ReflectionMethod || $value instanceof \ReflectionProperty || $value instanceof \ReflectionFunction || $value instanceof \ReflectionParameter || $value instanceof \ReflectionType || str_starts_with($className, 'Reflection')) { return sprintf('ReflectionObject(%s)', $className); } // Anonyme Klassen können auch Probleme verursachen if (str_contains($className, '@anonymous')) { $parentClass = get_parent_class($value); if ($parentClass !== false) { return sprintf('Anonymous(%s)', $parentClass); } return 'Anonymous'; } // Andere Objekte durch Klassenname ersetzen return $className; } // Arrays rekursiv bereinigen if (is_array($value)) { return array_map( fn ($item) => self::sanitizeValue($item), $value ); } // Primitives bleiben unverändert return $value; } /** * Gibt Klasse ohne Namespace zurück * Behandelt anonyme Klassen, indem das Interface/Parent-Class extrahiert wird */ public function getShortClass(): ?string { if ($this->class === null) { return null; } // Normalisiere Forward-Slashes zu Backslashes (falls vorhanden) // Dies ist wichtig, da manche Systeme Forward-Slashes verwenden $normalizedClass = str_replace('/', '\\', $this->class); // Anonyme Klassen erkennen: Format ist z.B. "App\Framework\Http\Next@anonymous/path/to/file.php:line$hash" // oder "Next@anonymous/path/to/file.php:line$hash" if (str_contains($normalizedClass, '@anonymous')) { // Extrahiere den Teil vor @anonymous (normalerweise das Interface mit vollständigem Namespace) $match = preg_match('/^([^@]+)@anonymous/', $normalizedClass, $matches); if ($match && isset($matches[1])) { $interfaceName = $matches[1]; // Entferne Namespace: Spalte am Backslash und nimm den letzten Teil $parts = explode('\\', $interfaceName); $shortName = end($parts); return $shortName . ' (anonymous)'; } return 'Anonymous'; } // Spalte am Backslash und nimm den letzten Teil (Klassenname ohne Namespace) $parts = explode('\\', $normalizedClass); $shortName = end($parts); // Sicherstellen, dass wir wirklich nur den letzten Teil zurückgeben // (falls explode() nicht funktioniert hat, z.B. bei Forward-Slashes) if ($shortName === $normalizedClass && str_contains($normalizedClass, '/')) { // Fallback: versuche es mit Forward-Slash $parts = explode('/', $normalizedClass); $shortName = end($parts); } return $shortName; } /** * Gibt Kurzform des File-Pfads zurück (relativ zum Project Root wenn möglich) */ public function getShortFile(): string { $projectRoot = self::projectRoot(); if (str_starts_with($this->file, $projectRoot)) { return substr($this->file, strlen($projectRoot) + 1); } return $this->file; } /** * True wenn Frame im vendor/ Verzeichnis liegt */ public function isVendorFrame(): bool { $projectRoot = self::projectRoot(); $vendorPath = $projectRoot . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR; return str_starts_with($this->file, $vendorPath); } /** * Gibt vollständigen Method/Function Call zurück (ohne Namespace) */ public function getCall(): string { $parts = []; if ($this->class !== null) { $parts[] = $this->getShortClass(); } if ($this->type !== null) { $parts[] = $this->type; } if ($this->function !== null) { $parts[] = $this->function . '()'; } return implode('', $parts); } /** * Formatiert Parameter für Display (kompakte Darstellung) */ public function formatParameters(int $maxArgs = self::MAX_ARGS, int $maxTotalLength = self::MAX_PARAMS_LENGTH): string { if (empty($this->args)) { return ''; } $formatted = []; $length = 0; foreach ($this->args as $index => $arg) { if ($index >= $maxArgs) { $formatted[] = '…'; break; } $param = $this->formatParameterForDisplay($arg); $length += strlen($param); $formatted[] = $param; if ($length > $maxTotalLength) { $formatted[] = '…'; break; } } return implode(', ', $formatted); } /** * Formatiert einzelnen Parameter für Display */ private function formatParameterForDisplay(mixed $value): string { return match (true) { is_string($value) => $this->formatStringParameter($value), is_int($value), is_float($value) => (string) $value, is_bool($value) => $value ? 'true' : 'false', is_null($value) => 'null', is_array($value) => sprintf('array(%d)', count($value)), is_resource($value) => sprintf('resource(%s)', get_resource_type($value)), is_object($value) => $this->formatObjectForDisplay($value), default => get_debug_type($value), }; } /** * Formatiert String-Parameter, entfernt Namespaces aus Klassennamen */ private function formatStringParameter(string $value): string { // Normalisiere Forward-Slashes zu Backslashes (falls vorhanden) $normalizedValue = str_replace('/', '\\', $value); // Wenn der String ein Klassename ist (vollständiger Namespace), entferne Namespace if (class_exists($normalizedValue) || interface_exists($normalizedValue) || enum_exists($normalizedValue)) { $parts = explode('\\', $normalizedValue); $shortName = end($parts); return sprintf("'%s'", $shortName); } // Prüfe, ob der String ein Namespace-Format hat (z.B. "App\Framework\Performance\PerformanceCategory") // Auch wenn die Klasse nicht existiert, entferne Namespace // Pattern: Beginnt mit Großbuchstaben, enthält Backslashes oder Forward-Slashes, endet mit Klassennamen if (preg_match('/^[A-Z][a-zA-Z0-9_\\\\\/]+$/', $normalizedValue) && (str_contains($normalizedValue, '\\') || str_contains($value, '/'))) { $parts = explode('\\', $normalizedValue); // Nur wenn es mehrere Teile gibt (Namespace vorhanden) if (count($parts) > 1) { $shortName = end($parts); return sprintf("'%s'", $shortName); } } // Closure-String-Format: "Closure(RouteDispatcher.php:77)" oder "Closure(/full/path/RouteDispatcher.php:77)" if (preg_match('/^Closure\(([^:]+):(\d+)\)$/', $value, $matches)) { $file = basename($matches[1]); $line = $matches[2]; return sprintf("Closure(%s:%s)", $file, $line); } // Lange Strings kürzen if (strlen($value) > self::MAX_STRING_LENGTH) { return sprintf("'%s...'", substr($value, 0, self::MAX_STRING_LENGTH)); } return sprintf("'%s'", $value); } /** * Formatiert Objekt für Display (kompakte Darstellung) */ private function formatObjectForDisplay(object $value): string { $className = get_class($value); // Entferne Namespace für bessere Lesbarkeit $parts = explode('\\', $className); $shortName = end($parts); // Anonyme Klassen if (str_contains($className, '@anonymous')) { $match = preg_match('/^([^@]+)@anonymous/', $className, $matches); if ($match && isset($matches[1])) { $interfaceName = $matches[1]; $interfaceParts = explode('\\', $interfaceName); $shortInterface = end($interfaceParts); return $shortInterface . ' (anonymous)'; } return 'Anonymous'; } return $shortName; } /** * Formatiert Funktionsnamen, entfernt Namespaces und verbessert Closure-Darstellung */ private function formatFunctionName(?string $function): string { if ($function === null) { return ''; } // Closures haben Format: {closure:Namespace\Class::method():line} oder {closure:Namespace/Class::method():line} if (preg_match('/\{closure:([^}]+)\}/', $function, $matches)) { $closureInfo = $matches[1]; // Normalisiere Forward-Slashes zu Backslashes $closureInfo = str_replace('/', '\\', $closureInfo); // Parse: App\Framework\Router\RouteDispatcher::executeController():77 if (preg_match('/^([^:]+)::([^(]+)\(\):(\d+)$/', $closureInfo, $closureMatches)) { $fullClass = $closureMatches[1]; $method = $closureMatches[2]; $line = $closureMatches[3]; // Entferne Namespace $classParts = explode('\\', $fullClass); $shortClass = end($classParts); return sprintf('{closure:%s::%s():%s}', $shortClass, $method, $line); } // Fallback: einfach Namespaces entfernen $closureInfo = preg_replace_callback( '/([A-Z][a-zA-Z0-9_\\\\]*)/', fn ($m) => $this->removeNamespaceFromClass($m[0]), $closureInfo ); return sprintf('{closure:%s}', $closureInfo); } return $function; } /** * Entfernt Namespace aus Klassennamen in Strings */ private function removeNamespaceFromClass(string $classString): string { // Normalisiere Forward-Slashes zu Backslashes $normalized = str_replace('/', '\\', $classString); $parts = explode('\\', $normalized); return end($parts); } /** * Kurzform für kompaktes Logging / JSON */ public function formatShort(): string { $call = $this->getCall() !== '' ? $this->getCall() : '{main}'; return sprintf('%s @ %s:%d', $call, $this->getShortFile(), $this->line); } /** * Formatiert für Display (HTML/Console) * Verwendet Standard PHP Stack Trace Format: ClassName->methodName($param1, $param2, ...) in file.php:line */ public function formatForDisplay(): string { $shortFile = $this->getShortFile(); $location = sprintf('%s:%d', $shortFile, $this->line); $params = $this->formatParameters(); // Wenn Klasse vorhanden if ($this->class !== null && $this->function !== null) { $className = $this->getShortClass(); $methodName = $this->formatFunctionName($this->function); $separator = $this->type === '::' ? '::' : '->'; $paramsStr = $params !== '' ? $params : ''; return sprintf('%s%s%s(%s) in %s', $className, $separator, $methodName, $paramsStr, $location); } // Wenn nur Funktion vorhanden if ($this->function !== null) { $methodName = $this->formatFunctionName($this->function); $paramsStr = $params !== '' ? $params : ''; return sprintf('%s(%s) in %s', $methodName, $paramsStr, $location); } // Wenn weder Klasse noch Funktion return $location; } /** * Konvertiert zu Array für JSON-Serialisierung * * @return array */ public function toArray(): array { $data = [ 'file' => $this->getShortFile(), 'full_file' => $this->file, 'line' => $this->line, ]; if ($this->function !== null) { $data['function'] = $this->function; } if ($this->class !== null) { $data['class'] = $this->getShortClass(); $data['full_class'] = $this->class; } if ($this->type !== null) { $data['type'] = $this->type; } if (! empty($this->args)) { $data['args'] = $this->serializeArgs(); } return $data; } /** * Serialisiert Arguments für Ausgabe * * @return array */ private function serializeArgs(): array { return array_map( fn ($arg) => $this->formatValueForOutput($arg), $this->args ); } /** * Formatiert Wert für Ausgabe (kompaktere Darstellung) */ private function formatValueForOutput(mixed $value): mixed { return match (true) { is_array($value) => sprintf('array(%d)', count($value)), is_resource($value) => sprintf('resource(%s)', get_resource_type($value)), is_string($value) && strlen($value) > 100 => substr($value, 0, 100) . '...', default => $value, }; } private static function projectRoot(): string { static $projectRoot = null; if ($projectRoot !== null) { return $projectRoot; } $projectRoot = dirname(__DIR__, 4); return $projectRoot; } }