Protokoll #9985
| ID | 9985 |
|---|---|
| Zeitstempel | 2025-12-23 14:14:46.776023 |
| Client | root |
| IP | Warning: Undefined array key "request_ip" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 Deprecated: htmlspecialchars(): Passing null to parameter #1 ($string) of type string is deprecated in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 13 |
| Modell | claude-sonnet-4-20250514 |
| Status | completed |
| Tokens | 12,676 (Input: 0, Output: 0) |
| Dauer | 104 ms |
| Request-Zeit | Warning: Undefined array key "request_timestamp" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 18 |
| Response-Zeit | - |
Warning: Undefined array key "error_message" in /var/www/dev.campus.systemische-tools.de/src/View/protokoll/show.php on line 22
Request
{
"event": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Infrastructure\/CodeAnalysis\/PhpFileParser.php",
"content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Infrastructure\\CodeAnalysis;\n\n\/\/ @responsibility: Tokenizer-basierte PHP-Dateianalyse mit Dependency-Extraktion\n\nfinal class PhpFileParser\n{\n \/** @var array<string, string> Maps short name to FQCN from use statements *\/\n private array $importMap = [];\n\n \/** @var string|null Current namespace *\/\n private ?string $currentNamespace = null;\n\n \/**\n * Parst eine PHP-Datei und extrahiert Metadaten inkl. Dependencies.\n *\n * @return array{\n * namespace: string|null,\n * classes: array<array{name: string, type: string, line: int}>,\n * functions: array<array{name: string, visibility: string|null, line: int}>,\n * uses: array<string>,\n * extends_class: string|null,\n * implements_interfaces: array<string>,\n * traits_used: array<string>,\n * constructor_deps: array<string>,\n * error: string|null\n * }\n *\/\n public function parse(string $filePath): array\n {\n $this->importMap = [];\n $this->currentNamespace = null;\n\n $result = [\n 'namespace' => null,\n 'classes' => [],\n 'functions' => [],\n 'uses' => [],\n 'extends_class' => null,\n 'implements_interfaces' => [],\n 'traits_used' => [],\n 'constructor_deps' => [],\n 'error' => null,\n ];\n\n if (!file_exists($filePath) || !is_readable($filePath)) {\n $result['error'] = 'Datei nicht lesbar';\n\n return $result;\n }\n\n $content = file_get_contents($filePath);\n if ($content === false) {\n $result['error'] = 'Datei konnte nicht gelesen werden';\n\n return $result;\n }\n\n try {\n $tokens = @token_get_all($content);\n } catch (\\Throwable $e) {\n $result['error'] = 'Token-Fehler: ' . $e->getMessage();\n\n return $result;\n }\n\n \/\/ First pass: Extract namespace and use statements for resolution\n $result['namespace'] = $this->extractNamespace($tokens);\n $this->currentNamespace = $result['namespace'];\n $result['uses'] = $this->extractUseStatements($tokens);\n\n \/\/ Build import map for FQCN resolution\n foreach ($result['uses'] as $fqcn) {\n $parts = explode('\\\\', $fqcn);\n $shortName = end($parts);\n $this->importMap[$shortName] = $fqcn;\n }\n\n \/\/ Second pass: Extract structural elements\n $result['classes'] = $this->extractClasses($tokens);\n $result['functions'] = $this->extractFunctions($tokens);\n\n \/\/ Third pass: Extract dependencies requiring resolution\n $result['extends_class'] = $this->extractExtends($tokens);\n $result['implements_interfaces'] = $this->extractImplements($tokens);\n $result['traits_used'] = $this->extractTraits($tokens);\n $result['constructor_deps'] = $this->extractConstructorDeps($tokens);\n\n return $result;\n }\n\n \/**\n * @param array<mixed> $tokens\n *\/\n private function extractNamespace(array $tokens): ?string\n {\n $namespace = '';\n $capturing = false;\n\n foreach ($tokens as $token) {\n if (is_array($token)) {\n if ($token[0] === T_NAMESPACE) {\n $capturing = true;\n\n continue;\n }\n\n if ($capturing) {\n if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_STRING) {\n $namespace .= $token[1];\n } elseif ($token[0] === T_NS_SEPARATOR) {\n $namespace .= '\\\\';\n }\n }\n } elseif ($capturing && ($token === ';' || $token === '{')) {\n break;\n }\n }\n\n return $namespace !== '' ? $namespace : null;\n }\n\n \/**\n * Extracts use statements (top-level imports only).\n *\n * @param array<mixed> $tokens\n * @return array<string>\n *\/\n private function extractUseStatements(array $tokens): array\n {\n $uses = [];\n $count = count($tokens);\n $braceDepth = 0;\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n \/\/ Track brace depth to distinguish top-level use from trait use\n if (!is_array($token)) {\n if ($token === '{') {\n $braceDepth++;\n } elseif ($token === '}') {\n $braceDepth--;\n }\n\n continue;\n }\n\n \/\/ Only process top-level use statements\n if ($token[0] !== T_USE || $braceDepth > 0) {\n continue;\n }\n\n \/\/ Skip function\/const imports\n $nextToken = $this->findNextNonWhitespace($tokens, $i);\n if ($nextToken !== null && is_array($tokens[$nextToken])) {\n if ($tokens[$nextToken][0] === T_FUNCTION || $tokens[$nextToken][0] === T_CONST) {\n continue;\n }\n }\n\n \/\/ Extract the use statement(s)\n $useResult = $this->parseUseStatement($tokens, $i);\n foreach ($useResult['fqcns'] as $fqcn) {\n $uses[] = $fqcn;\n }\n }\n\n return array_unique($uses);\n }\n\n \/**\n * Parse a single use statement, handling grouped imports.\n *\n * @param array<mixed> $tokens\n * @return array{fqcns: array<string>, endIndex: int}\n *\/\n private function parseUseStatement(array $tokens, int $startIndex): array\n {\n $fqcns = [];\n $count = count($tokens);\n $baseNamespace = '';\n $currentName = '';\n $inGroup = false;\n\n for ($i = $startIndex + 1; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if ($token === ';') {\n if ($currentName !== '') {\n $fqcns[] = $inGroup ? $baseNamespace . $currentName : $currentName;\n }\n\n break;\n }\n\n if ($token === '{') {\n $baseNamespace = $currentName;\n $currentName = '';\n $inGroup = true;\n\n continue;\n }\n\n if ($token === '}') {\n if ($currentName !== '') {\n $fqcns[] = $baseNamespace . $currentName;\n }\n\n continue;\n }\n\n if ($token === ',') {\n if ($currentName !== '') {\n $fqcns[] = $inGroup ? $baseNamespace . $currentName : $currentName;\n }\n $currentName = '';\n\n continue;\n }\n\n if (!is_array($token)) {\n continue;\n }\n\n \/\/ Skip 'as' aliases - we only care about the original FQCN\n if ($token[0] === T_AS) {\n \/\/ Skip until comma or semicolon\n for ($j = $i + 1; $j < $count; $j++) {\n if ($tokens[$j] === ',' || $tokens[$j] === ';' || $tokens[$j] === '}') {\n $i = $j - 1;\n\n break;\n }\n }\n\n continue;\n }\n\n if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) {\n $currentName .= ltrim($token[1], '\\\\');\n } elseif ($token[0] === T_STRING) {\n $currentName .= $token[1];\n } elseif ($token[0] === T_NS_SEPARATOR) {\n $currentName .= '\\\\';\n }\n }\n\n return ['fqcns' => $fqcns, 'endIndex' => $i ?? $startIndex];\n }\n\n \/**\n * @param array<mixed> $tokens\n * @return array<array{name: string, type: string, line: int}>\n *\/\n private function extractClasses(array $tokens): array\n {\n $classes = [];\n $count = count($tokens);\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if (!is_array($token)) {\n continue;\n }\n\n $type = match ($token[0]) {\n T_CLASS => 'class',\n T_INTERFACE => 'interface',\n T_TRAIT => 'trait',\n T_ENUM => 'enum',\n default => null,\n };\n\n if ($type === null) {\n continue;\n }\n\n \/\/ Skip anonymous class statements\n $prevIndex = $this->findPrevNonWhitespace($tokens, $i);\n if ($prevIndex !== null && is_array($tokens[$prevIndex]) && $tokens[$prevIndex][0] === T_NEW) {\n continue;\n }\n\n \/\/ Find class name\n for ($j = $i + 1; $j < $count; $j++) {\n if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {\n $classes[] = [\n 'name' => $tokens[$j][1],\n 'type' => $type,\n 'line' => $token[2],\n ];\n\n break;\n }\n }\n }\n\n return $classes;\n }\n\n \/**\n * @param array<mixed> $tokens\n * @return array<array{name: string, visibility: string|null, line: int}>\n *\/\n private function extractFunctions(array $tokens): array\n {\n $functions = [];\n $count = count($tokens);\n $braceDepth = 0;\n $inClass = false;\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if (!is_array($token)) {\n if ($token === '{') {\n $braceDepth++;\n } elseif ($token === '}') {\n $braceDepth--;\n if ($braceDepth === 0) {\n $inClass = false;\n }\n }\n\n continue;\n }\n\n if (in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) {\n $inClass = true;\n }\n\n ... [TRUNCATED-5b003c1dd9e0459a]"
}
}
Response
{
"tool_response": {
"type": "update",
"filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Infrastructure\/CodeAnalysis\/PhpFileParser.php",
"content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Infrastructure\\CodeAnalysis;\n\n\/\/ @responsibility: Tokenizer-basierte PHP-Dateianalyse mit Dependency-Extraktion\n\nfinal class PhpFileParser\n{\n \/** @var array<string, string> Maps short name to FQCN from use statements *\/\n private array $importMap = [];\n\n \/** @var string|null Current namespace *\/\n private ?string $currentNamespace = null;\n\n \/**\n * Parst eine PHP-Datei und extrahiert Metadaten inkl. Dependencies.\n *\n * @return array{\n * namespace: string|null,\n * classes: array<array{name: string, type: string, line: int}>,\n * functions: array<array{name: string, visibility: string|null, line: int}>,\n * uses: array<string>,\n * extends_class: string|null,\n * implements_interfaces: array<string>,\n * traits_used: array<string>,\n * constructor_deps: array<string>,\n * error: string|null\n * }\n *\/\n public function parse(string $filePath): array\n {\n $this->importMap = [];\n $this->currentNamespace = null;\n\n $result = [\n 'namespace' => null,\n 'classes' => [],\n 'functions' => [],\n 'uses' => [],\n 'extends_class' => null,\n 'implements_interfaces' => [],\n 'traits_used' => [],\n 'constructor_deps' => [],\n 'error' => null,\n ];\n\n if (!file_exists($filePath) || !is_readable($filePath)) {\n $result['error'] = 'Datei nicht lesbar';\n\n return $result;\n }\n\n $content = file_get_contents($filePath);\n if ($content === false) {\n $result['error'] = 'Datei konnte nicht gelesen werden';\n\n return $result;\n }\n\n try {\n $tokens = @token_get_all($content);\n } catch (\\Throwable $e) {\n $result['error'] = 'Token-Fehler: ' . $e->getMessage();\n\n return $result;\n }\n\n \/\/ First pass: Extract namespace and use statements for resolution\n $result['namespace'] = $this->extractNamespace($tokens);\n $this->currentNamespace = $result['namespace'];\n $result['uses'] = $this->extractUseStatements($tokens);\n\n \/\/ Build import map for FQCN resolution\n foreach ($result['uses'] as $fqcn) {\n $parts = explode('\\\\', $fqcn);\n $shortName = end($parts);\n $this->importMap[$shortName] = $fqcn;\n }\n\n \/\/ Second pass: Extract structural elements\n $result['classes'] = $this->extractClasses($tokens);\n $result['functions'] = $this->extractFunctions($tokens);\n\n \/\/ Third pass: Extract dependencies requiring resolution\n $result['extends_class'] = $this->extractExtends($tokens);\n $result['implements_interfaces'] = $this->extractImplements($tokens);\n $result['traits_used'] = $this->extractTraits($tokens);\n $result['constructor_deps'] = $this->extractConstructorDeps($tokens);\n\n return $result;\n }\n\n \/**\n * @param array<mixed> $tokens\n *\/\n private function extractNamespace(array $tokens): ?string\n {\n $namespace = '';\n $capturing = false;\n\n foreach ($tokens as $token) {\n if (is_array($token)) {\n if ($token[0] === T_NAMESPACE) {\n $capturing = true;\n\n continue;\n }\n\n if ($capturing) {\n if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_STRING) {\n $namespace .= $token[1];\n } elseif ($token[0] === T_NS_SEPARATOR) {\n $namespace .= '\\\\';\n }\n }\n } elseif ($capturing && ($token === ';' || $token === '{')) {\n break;\n }\n }\n\n return $namespace !== '' ? $namespace : null;\n }\n\n \/**\n * Extracts use statements (top-level imports only).\n *\n * @param array<mixed> $tokens\n * @return array<string>\n *\/\n private function extractUseStatements(array $tokens): array\n {\n $uses = [];\n $count = count($tokens);\n $braceDepth = 0;\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n \/\/ Track brace depth to distinguish top-level use from trait use\n if (!is_array($token)) {\n if ($token === '{') {\n $braceDepth++;\n } elseif ($token === '}') {\n $braceDepth--;\n }\n\n continue;\n }\n\n \/\/ Only process top-level use statements\n if ($token[0] !== T_USE || $braceDepth > 0) {\n continue;\n }\n\n \/\/ Skip function\/const imports\n $nextToken = $this->findNextNonWhitespace($tokens, $i);\n if ($nextToken !== null && is_array($tokens[$nextToken])) {\n if ($tokens[$nextToken][0] === T_FUNCTION || $tokens[$nextToken][0] === T_CONST) {\n continue;\n }\n }\n\n \/\/ Extract the use statement(s)\n $useResult = $this->parseUseStatement($tokens, $i);\n foreach ($useResult['fqcns'] as $fqcn) {\n $uses[] = $fqcn;\n }\n }\n\n return array_unique($uses);\n }\n\n \/**\n * Parse a single use statement, handling grouped imports.\n *\n * @param array<mixed> $tokens\n * @return array{fqcns: array<string>, endIndex: int}\n *\/\n private function parseUseStatement(array $tokens, int $startIndex): array\n {\n $fqcns = [];\n $count = count($tokens);\n $baseNamespace = '';\n $currentName = '';\n $inGroup = false;\n\n for ($i = $startIndex + 1; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if ($token === ';') {\n if ($currentName !== '') {\n $fqcns[] = $inGroup ? $baseNamespace . $currentName : $currentName;\n }\n\n break;\n }\n\n if ($token === '{') {\n $baseNamespace = $currentName;\n $currentName = '';\n $inGroup = true;\n\n continue;\n }\n\n if ($token === '}') {\n if ($currentName !== '') {\n $fqcns[] = $baseNamespace . $currentName;\n }\n\n continue;\n }\n\n if ($token === ',') {\n if ($currentName !== '') {\n $fqcns[] = $inGroup ? $baseNamespace . $currentName : $currentName;\n }\n $currentName = '';\n\n continue;\n }\n\n if (!is_array($token)) {\n continue;\n }\n\n \/\/ Skip 'as' aliases - we only care about the original FQCN\n if ($token[0] === T_AS) {\n \/\/ Skip until comma or semicolon\n for ($j = $i + 1; $j < $count; $j++) {\n if ($tokens[$j] === ',' || $tokens[$j] === ';' || $tokens[$j] === '}') {\n $i = $j - 1;\n\n break;\n }\n }\n\n continue;\n }\n\n if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) {\n $currentName .= ltrim($token[1], '\\\\');\n } elseif ($token[0] === T_STRING) {\n $currentName .= $token[1];\n } elseif ($token[0] === T_NS_SEPARATOR) {\n $currentName .= '\\\\';\n }\n }\n\n return ['fqcns' => $fqcns, 'endIndex' => $i ?? $startIndex];\n }\n\n \/**\n * @param array<mixed> $tokens\n * @return array<array{name: string, type: string, line: int}>\n *\/\n private function extractClasses(array $tokens): array\n {\n $classes = [];\n $count = count($tokens);\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if (!is_array($token)) {\n continue;\n }\n\n $type = match ($token[0]) {\n T_CLASS => 'class',\n T_INTERFACE => 'interface',\n T_TRAIT => 'trait',\n T_ENUM => 'enum',\n default => null,\n };\n\n if ($type === null) {\n continue;\n }\n\n \/\/ Skip anonymous class statements\n $prevIndex = $this->findPrevNonWhitespace($tokens, $i);\n if ($prevIndex !== null && is_array($tokens[$prevIndex]) && $tokens[$prevIndex][0] === T_NEW) {\n continue;\n }\n\n \/\/ Find class name\n for ($j = $i + 1; $j < $count; $j++) {\n if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {\n $classes[] = [\n 'name' => $tokens[$j][1],\n 'type' => $type,\n 'line' => $token[2],\n ];\n\n break;\n }\n }\n }\n\n return $classes;\n }\n\n \/**\n * @param array<mixed> $tokens\n * @return array<array{name: string, visibility: string|null, line: int}>\n *\/\n private function extractFunctions(array $tokens): array\n {\n $functions = [];\n $count = count($tokens);\n $braceDepth = 0;\n $inClass = false;\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if (!is_array($token)) {\n if ($token === '{') {\n $braceDepth++;\n } elseif ($token === '}') {\n $braceDepth--;\n if ($braceDepth === 0) {\n $inClass = false;\n }\n }\n\n continue;\n }\n\n if (in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) {\n $inClass = true;\n }\n\n ... [TRUNCATED-5b003c1dd9e0459a]",
"structuredPatch": [
{
"oldStart": 4,
"oldLines": 26,
"newStart": 4,
"newLines": 45,
"lines": [
" ",
" namespace Infrastructure\\CodeAnalysis;",
" ",
"-\/\/ @responsibility: Tokenizer-basierte PHP-Dateianalyse",
"+\/\/ @responsibility: Tokenizer-basierte PHP-Dateianalyse mit Dependency-Extraktion",
" ",
" final class PhpFileParser",
" {",
"+ \/** @var array<string, string> Maps short name to FQCN from use statements *\/",
"+ private array $importMap = [];",
"+",
"+ \/** @var string|null Current namespace *\/",
"+ private ?string $currentNamespace = null;",
"+",
" \/**",
"- * Parst eine PHP-Datei und extrahiert Metadaten.",
"+ * Parst eine PHP-Datei und extrahiert Metadaten inkl. Dependencies.",
" *",
" * @return array{",
" * namespace: string|null,",
" * classes: array<array{name: string, type: string, line: int}>,",
" * functions: array<array{name: string, visibility: string|null, line: int}>,",
"+ * uses: array<string>,",
"+ * extends_class: string|null,",
"+ * implements_interfaces: array<string>,",
"+ * traits_used: array<string>,",
"+ * constructor_deps: array<string>,",
" * error: string|null",
" * }",
" *\/",
" public function parse(string $filePath): array",
" {",
"+ $this->importMap = [];",
"+ $this->currentNamespace = null;",
"+",
" $result = [",
" 'namespace' => null,",
" 'classes' => [],",
" 'functions' => [],",
"+ 'uses' => [],",
"+ 'extends_class' => null,",
"+ 'implements_interfaces' => [],",
"+ 'traits_used' => [],",
"+ 'constructor_deps' => [],",
" 'error' => null,",
" ];",
" "
]
},
{
"oldStart": 48,
"oldLines": 10,
"newStart": 67,
"newLines": 28,
"lines": [
" return $result;",
" }",
" ",
"+ \/\/ First pass: Extract namespace and use statements for resolution",
" $result['namespace'] = $this->extractNamespace($tokens);",
"+ $this->currentNamespace = $result['namespace'];",
"+ $result['uses'] = $this->extractUseStatements($tokens);",
"+",
"+ \/\/ Build import map for FQCN resolution",
"+ foreach ($result['uses'] as $fqcn) {",
"+ $parts = explode('\\\\', $fqcn);",
"+ $shortName = end($parts);",
"+ $this->importMap[$shortName] = $fqcn;",
"+ }",
"+",
"+ \/\/ Second pass: Extract structural elements",
" $result['classes'] = $this->extractClasses($tokens);",
" $result['functions'] = $this->extractFunctions($tokens);",
" ",
"+ \/\/ Third pass: Extract dependencies requiring resolution",
"+ $result['extends_class'] = $this->extractExtends($tokens);",
"+ $result['implements_interfaces'] = $this->extractImplements($tokens);",
"+ $result['traits_used'] = $this->extractTraits($tokens);",
"+ $result['constructor_deps'] = $this->extractConstructorDeps($tokens);",
"+",
" return $result;",
" }",
" "
]
},
{
"oldStart": 67,
"oldLines": 6,
"newStart": 104,
"newLines": 7,
"lines": [
" if (is_array($token)) {",
" if ($token[0] === T_NAMESPACE) {",
" $capturing = true;",
"+",
" continue;",
" }",
" "
]
},
{
"oldStart": 86,
"oldLines": 7,
"newStart": 124,
"newLines": 136,
"lines": [
" }",
" ",
" \/**",
"+ * Extracts use statements (top-level imports only).",
"+ *",
" * @param array<mixed> $tokens",
"+ * @return array<string>",
"+ *\/",
"+ private function extractUseStatements(array $tokens): array",
"+ {",
"+ $uses = [];",
"+ $count = count($tokens);",
"+ $braceDepth = 0;",
"+",
"+ for ($i = 0; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ \/\/ Track brace depth to distinguish top-level use from trait use",
"+ if (!is_array($token)) {",
"+ if ($token === '{') {",
"+ $braceDepth++;",
"+ } elseif ($token === '}') {",
"+ $braceDepth--;",
"+ }",
"+",
"+ continue;",
"+ }",
"+",
"+ \/\/ Only process top-level use statements",
"+ if ($token[0] !== T_USE || $braceDepth > 0) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Skip function\/const imports",
"+ $nextToken = $this->findNextNonWhitespace($tokens, $i);",
"+ if ($nextToken !== null && is_array($tokens[$nextToken])) {",
"+ if ($tokens[$nextToken][0] === T_FUNCTION || $tokens[$nextToken][0] === T_CONST) {",
"+ continue;",
"+ }",
"+ }",
"+",
"+ \/\/ Extract the use statement(s)",
"+ $useResult = $this->parseUseStatement($tokens, $i);",
"+ foreach ($useResult['fqcns'] as $fqcn) {",
"+ $uses[] = $fqcn;",
"+ }",
"+ }",
"+",
"+ return array_unique($uses);",
"+ }",
"+",
"+ \/**",
"+ * Parse a single use statement, handling grouped imports.",
"+ *",
"+ * @param array<mixed> $tokens",
"+ * @return array{fqcns: array<string>, endIndex: int}",
"+ *\/",
"+ private function parseUseStatement(array $tokens, int $startIndex): array",
"+ {",
"+ $fqcns = [];",
"+ $count = count($tokens);",
"+ $baseNamespace = '';",
"+ $currentName = '';",
"+ $inGroup = false;",
"+",
"+ for ($i = $startIndex + 1; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ if ($token === ';') {",
"+ if ($currentName !== '') {",
"+ $fqcns[] = $inGroup ? $baseNamespace . $currentName : $currentName;",
"+ }",
"+",
"+ break;",
"+ }",
"+",
"+ if ($token === '{') {",
"+ $baseNamespace = $currentName;",
"+ $currentName = '';",
"+ $inGroup = true;",
"+",
"+ continue;",
"+ }",
"+",
"+ if ($token === '}') {",
"+ if ($currentName !== '') {",
"+ $fqcns[] = $baseNamespace . $currentName;",
"+ }",
"+",
"+ continue;",
"+ }",
"+",
"+ if ($token === ',') {",
"+ if ($currentName !== '') {",
"+ $fqcns[] = $inGroup ? $baseNamespace . $currentName : $currentName;",
"+ }",
"+ $currentName = '';",
"+",
"+ continue;",
"+ }",
"+",
"+ if (!is_array($token)) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Skip 'as' aliases - we only care about the original FQCN",
"+ if ($token[0] === T_AS) {",
"+ \/\/ Skip until comma or semicolon",
"+ for ($j = $i + 1; $j < $count; $j++) {",
"+ if ($tokens[$j] === ',' || $tokens[$j] === ';' || $tokens[$j] === '}') {",
"+ $i = $j - 1;",
"+",
"+ break;",
"+ }",
"+ }",
"+",
"+ continue;",
"+ }",
"+",
"+ if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) {",
"+ $currentName .= ltrim($token[1], '\\\\');",
"+ } elseif ($token[0] === T_STRING) {",
"+ $currentName .= $token[1];",
"+ } elseif ($token[0] === T_NS_SEPARATOR) {",
"+ $currentName .= '\\\\';",
"+ }",
"+ }",
"+",
"+ return ['fqcns' => $fqcns, 'endIndex' => $i ?? $startIndex];",
"+ }",
"+",
"+ \/**",
"+ * @param array<mixed> $tokens",
" * @return array<array{name: string, type: string, line: int}>",
" *\/",
" private function extractClasses(array $tokens): array"
]
},
{
"oldStart": 113,
"oldLines": 13,
"newStart": 280,
"newLines": 13,
"lines": [
" continue;",
" }",
" ",
"- \/\/ Prüfen ob es kein anonymes Class-Statement ist",
"+ \/\/ Skip anonymous class statements",
" $prevIndex = $this->findPrevNonWhitespace($tokens, $i);",
" if ($prevIndex !== null && is_array($tokens[$prevIndex]) && $tokens[$prevIndex][0] === T_NEW) {",
" continue;",
" }",
" ",
"- \/\/ Klassennamen finden",
"+ \/\/ Find class name",
" for ($j = $i + 1; $j < $count; $j++) {",
" if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {",
" $classes[] = ["
]
},
{
"oldStart": 127,
"oldLines": 6,
"newStart": 294,
"newLines": 7,
"lines": [
" 'type' => $type,",
" 'line' => $token[2],",
" ];",
"+",
" break;",
" }",
" }"
]
},
{
"oldStart": 158,
"oldLines": 10,
"newStart": 326,
"newLines": 10,
"lines": [
" $inClass = false;",
" }",
" }",
"+",
" continue;",
" }",
" ",
"- \/\/ Klasse\/Interface\/Trait betreten",
" if (in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) {",
" $inClass = true;",
" }"
]
},
{
"oldStart": 170,
"oldLines": 7,
"newStart": 338,
"newLines": 6,
"lines": [
" continue;",
" }",
" ",
"- \/\/ Visibility ermitteln",
" $visibility = null;",
" if ($inClass) {",
" for ($j = $i - 1; $j >= 0; $j--) {"
]
},
{
"oldStart": 179,
"oldLines": 14,
"newStart": 346,
"newLines": 17,
"lines": [
" }",
" if ($tokens[$j][0] === T_PUBLIC) {",
" $visibility = 'public';",
"+",
" break;",
" }",
" if ($tokens[$j][0] === T_PROTECTED) {",
" $visibility = 'protected';",
"+",
" break;",
" }",
" if ($tokens[$j][0] === T_PRIVATE) {",
" $visibility = 'private';",
"+",
" break;",
" }",
" if ($tokens[$j][0] !== T_WHITESPACE && $tokens[$j][0] !== T_STATIC && $tokens[$j][0] !== T_FINAL && $tokens[$j][0] !== T_ABSTRACT) {"
]
},
{
"oldStart": 195,
"oldLines": 7,
"newStart": 365,
"newLines": 6,
"lines": [
" }",
" }",
" ",
"- \/\/ Funktionsnamen finden",
" for ($j = $i + 1; $j < $count; $j++) {",
" if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {",
" $functions[] = ["
]
},
{
"oldStart": 203,
"oldLines": 9,
"newStart": 372,
"newLines": 9,
"lines": [
" 'visibility' => $visibility,",
" 'line' => $token[2],",
" ];",
"+",
" break;",
" }",
"- \/\/ Anonyme Funktion (kein Name)",
" if ($tokens[$j] === '(') {",
" break;",
" }"
]
},
{
"oldStart": 216,
"oldLines": 8,
"newStart": 385,
"newLines": 351,
"lines": [
" }",
" ",
" \/**",
"+ * Extract the parent class (extends clause).",
"+ *",
" * @param array<mixed> $tokens",
" *\/",
"+ private function extractExtends(array $tokens): ?string",
"+ {",
"+ $count = count($tokens);",
"+",
"+ for ($i = 0; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ if (!is_array($token) || $token[0] !== T_EXTENDS) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Find the class name after extends",
"+ $className = $this->extractTypeName($tokens, $i + 1);",
"+ if ($className !== null) {",
"+ return $this->resolveFqcn($className);",
"+ }",
"+ }",
"+",
"+ return null;",
"+ }",
"+",
"+ \/**",
"+ * Extract implemented interfaces.",
"+ *",
"+ * @param array<mixed> $tokens",
"+ * @return array<string>",
"+ *\/",
"+ private function extractImplements(array $tokens): array",
"+ {",
"+ $interfaces = [];",
"+ $count = count($tokens);",
"+",
"+ for ($i = 0; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ if (!is_array($token) || $token[0] !== T_IMPLEMENTS) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Collect interface names until { or extends",
"+ for ($j = $i + 1; $j < $count; $j++) {",
"+ if ($tokens[$j] === '{') {",
"+ break;",
"+ }",
"+",
"+ if ($tokens[$j] === ',') {",
"+ continue;",
"+ }",
"+",
"+ if (!is_array($tokens[$j])) {",
"+ continue;",
"+ }",
"+",
"+ if ($tokens[$j][0] === T_EXTENDS) {",
"+ break;",
"+ }",
"+",
"+ if (in_array($tokens[$j][0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) {",
"+ $interfaces[] = $this->resolveFqcn($tokens[$j][1]);",
"+ }",
"+ }",
"+",
"+ break;",
"+ }",
"+",
"+ return array_unique($interfaces);",
"+ }",
"+",
"+ \/**",
"+ * Extract traits used inside class body.",
"+ *",
"+ * @param array<mixed> $tokens",
"+ * @return array<string>",
"+ *\/",
"+ private function extractTraits(array $tokens): array",
"+ {",
"+ $traits = [];",
"+ $count = count($tokens);",
"+ $braceDepth = 0;",
"+ $inClass = false;",
"+",
"+ for ($i = 0; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ if (!is_array($token)) {",
"+ if ($token === '{') {",
"+ $braceDepth++;",
"+ } elseif ($token === '}') {",
"+ $braceDepth--;",
"+ if ($braceDepth === 0) {",
"+ $inClass = false;",
"+ }",
"+ }",
"+",
"+ continue;",
"+ }",
"+",
"+ if (in_array($token[0], [T_CLASS, T_TRAIT, T_ENUM], true)) {",
"+ $inClass = true;",
"+",
"+ continue;",
"+ }",
"+",
"+ \/\/ T_USE inside class body = trait use",
"+ if ($token[0] === T_USE && $inClass && $braceDepth === 1) {",
"+ \/\/ Collect trait names until ; or {",
"+ for ($j = $i + 1; $j < $count; $j++) {",
"+ if ($tokens[$j] === ';' || $tokens[$j] === '{') {",
"+ break;",
"+ }",
"+",
"+ if ($tokens[$j] === ',') {",
"+ continue;",
"+ }",
"+",
"+ if (!is_array($tokens[$j])) {",
"+ continue;",
"+ }",
"+",
"+ if (in_array($tokens[$j][0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) {",
"+ $traits[] = $this->resolveFqcn($tokens[$j][1]);",
"+ }",
"+ }",
"+ }",
"+ }",
"+",
"+ return array_unique($traits);",
"+ }",
"+",
"+ \/**",
"+ * Extract constructor parameter type hints (DI dependencies).",
"+ *",
"+ * @param array<mixed> $tokens",
"+ * @return array<string>",
"+ *\/",
"+ private function extractConstructorDeps(array $tokens): array",
"+ {",
"+ $deps = [];",
"+ $count = count($tokens);",
"+ $braceDepth = 0;",
"+ $inClass = false;",
"+",
"+ for ($i = 0; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ if (!is_array($token)) {",
"+ if ($token === '{') {",
"+ $braceDepth++;",
"+ } elseif ($token === '}') {",
"+ $braceDepth--;",
"+ if ($braceDepth === 0) {",
"+ $inClass = false;",
"+ }",
"+ }",
"+",
"+ continue;",
"+ }",
"+",
"+ if (in_array($token[0], [T_CLASS, T_TRAIT, T_ENUM], true)) {",
"+ $inClass = true;",
"+",
"+ continue;",
"+ }",
"+",
"+ \/\/ Look for T_FUNCTION with name __construct",
"+ if ($token[0] !== T_FUNCTION) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Check if it's __construct",
"+ $funcName = $this->findNextString($tokens, $i);",
"+ if ($funcName !== '__construct') {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Find opening parenthesis",
"+ $parenStart = null;",
"+ for ($j = $i + 1; $j < $count; $j++) {",
"+ if ($tokens[$j] === '(') {",
"+ $parenStart = $j;",
"+",
"+ break;",
"+ }",
"+ }",
"+",
"+ if ($parenStart === null) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Extract type hints from parameters",
"+ $parenDepth = 1;",
"+ $lastType = null;",
"+",
"+ for ($j = $parenStart + 1; $j < $count && $parenDepth > 0; $j++) {",
"+ $paramToken = $tokens[$j];",
"+",
"+ if ($paramToken === '(') {",
"+ $parenDepth++;",
"+",
"+ continue;",
"+ }",
"+ if ($paramToken === ')') {",
"+ $parenDepth--;",
"+",
"+ continue;",
"+ }",
"+",
"+ if (!is_array($paramToken)) {",
"+ continue;",
"+ }",
"+",
"+ \/\/ Type hint tokens (skip built-in types)",
"+ if (in_array($paramToken[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) {",
"+ $typeName = $paramToken[1];",
"+",
"+ \/\/ Skip built-in types",
"+ if (!$this->isBuiltInType($typeName)) {",
"+ $lastType = $typeName;",
"+ }",
"+ }",
"+",
"+ \/\/ Variable = end of parameter, save type if found",
"+ if ($paramToken[0] === T_VARIABLE && $lastType !== null) {",
"+ $deps[] = $this->resolveFqcn($lastType);",
"+ $lastType = null;",
"+ }",
"+ }",
"+",
"+ \/\/ Only process first constructor found",
"+ break;",
"+ }",
"+",
"+ return array_unique($deps);",
"+ }",
"+",
"+ \/**",
"+ * Resolve a short or qualified name to FQCN.",
"+ *\/",
"+ private function resolveFqcn(string $name): string",
"+ {",
"+ \/\/ Already fully qualified",
"+ if (str_starts_with($name, '\\\\')) {",
"+ return ltrim($name, '\\\\');",
"+ }",
"+",
"+ \/\/ Already contains namespace separator (qualified name)",
"+ if (str_contains($name, '\\\\')) {",
"+ return $name;",
"+ }",
"+",
"+ \/\/ Check import map",
"+ if (isset($this->importMap[$name])) {",
"+ return $this->importMap[$name];",
"+ }",
"+",
"+ \/\/ Default to current namespace + name",
"+ if ($this->currentNamespace !== null) {",
"+ return $this->currentNamespace . '\\\\' . $name;",
"+ }",
"+",
"+ return $name;",
"+ }",
"+",
"+ \/**",
"+ * Check if a type name is a PHP built-in type.",
"+ *\/",
"+ private function isBuiltInType(string $type): bool",
"+ {",
"+ $builtIn = [",
"+ 'int', 'float', 'bool', 'string', 'array', 'object', 'callable',",
"+ 'iterable', 'void', 'null', 'mixed', 'never', 'true', 'false',",
"+ 'self', 'static', 'parent',",
"+ ];",
"+",
"+ return in_array(strtolower($type), $builtIn, true);",
"+ }",
"+",
"+ \/**",
"+ * Extract a type name starting from a position.",
"+ *",
"+ * @param array<mixed> $tokens",
"+ *\/",
"+ private function extractTypeName(array $tokens, int $startIndex): ?string",
"+ {",
"+ $count = count($tokens);",
"+",
"+ for ($i = $startIndex; $i < $count; $i++) {",
"+ $token = $tokens[$i];",
"+",
"+ if (!is_array($token)) {",
"+ continue;",
"+ }",
"+",
"+ if ($token[0] === T_WHITESPACE) {",
"+ continue;",
"+ }",
"+",
"+ if (in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED], true)) {",
"+ return $token[1];",
"+ }",
"+",
"+ \/\/ Stop at unexpected token",
"+ break;",
"+ }",
"+",
"+ return null;",
"+ }",
"+",
"+ \/**",
"+ * Find next T_STRING token and return its value.",
"+ *",
"+ * @param array<mixed> $tokens",
"+ *\/",
"+ private function findNextString(array $tokens, int $startIndex): ?string",
"+ {",
"+ $count = count($tokens);",
"+",
"+ for ($i = $startIndex + 1; $i < $count; $i++) {",
"+ if (!is_array($tokens[$i])) {",
"+ if ($tokens[$i] === '(') {",
"+ return null; \/\/ Anonymous function",
"+ }",
"+",
"+ continue;",
"+ }",
"+",
"+ if ($tokens[$i][0] === T_STRING) {",
"+ return $tokens[$i][1];",
"+ }",
"+",
"+ if ($tokens[$i][0] !== T_WHITESPACE) {",
"+ break;",
"+ }",
"+ }",
"+",
"+ return null;",
"+ }",
"+",
"+ \/**",
"+ * @param array<mixed> $tokens",
"+ *\/",
" private function findPrevNonWhitespace(array $tokens, int $index): ?int",
" {",
" for ($i = $index - 1; $i >= 0; $i--) {"
]
},
{
"oldStart": 228,
"oldLines": 4,
"newStart": 740,
"newLines": 20,
"lines": [
" ",
" return null;",
" }",
"+",
"+ \/**",
"+ * @param array<mixed> $tokens",
"+ *\/",
"+ private function findNextNonWhitespace(array $tokens, int $index): ?int",
"+ {",
"+ $count = count($tokens);",
"+",
"+ for ($i = $index + 1; $i < $count; $i++) {",
"+ if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_WHITESPACE) {",
"+ return $i;",
"+ }",
"+ }",
"+",
"+ return null;",
"+ }",
" }"
]
}
],
"originalFile": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Infrastructure\\CodeAnalysis;\n\n\/\/ @responsibility: Tokenizer-basierte PHP-Dateianalyse\n\nfinal class PhpFileParser\n{\n \/**\n * Parst eine PHP-Datei und extrahiert Metadaten.\n *\n * @return array{\n * namespace: string|null,\n * classes: array<array{name: string, type: string, line: int}>,\n * functions: array<array{name: string, visibility: string|null, line: int}>,\n * error: string|null\n * }\n *\/\n public function parse(string $filePath): array\n {\n $result = [\n 'namespace' => null,\n 'classes' => [],\n 'functions' => [],\n 'error' => null,\n ];\n\n if (!file_exists($filePath) || !is_readable($filePath)) {\n $result['error'] = 'Datei nicht lesbar';\n\n return $result;\n }\n\n $content = file_get_contents($filePath);\n if ($content === false) {\n $result['error'] = 'Datei konnte nicht gelesen werden';\n\n return $result;\n }\n\n try {\n $tokens = @token_get_all($content);\n } catch (\\Throwable $e) {\n $result['error'] = 'Token-Fehler: ' . $e->getMessage();\n\n return $result;\n }\n\n $result['namespace'] = $this->extractNamespace($tokens);\n $result['classes'] = $this->extractClasses($tokens);\n $result['functions'] = $this->extractFunctions($tokens);\n\n return $result;\n }\n\n \/**\n * @param array<mixed> $tokens\n *\/\n private function extractNamespace(array $tokens): ?string\n {\n $namespace = '';\n $capturing = false;\n\n foreach ($tokens as $token) {\n if (is_array($token)) {\n if ($token[0] === T_NAMESPACE) {\n $capturing = true;\n continue;\n }\n\n if ($capturing) {\n if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_STRING) {\n $namespace .= $token[1];\n } elseif ($token[0] === T_NS_SEPARATOR) {\n $namespace .= '\\\\';\n }\n }\n } elseif ($capturing && ($token === ';' || $token === '{')) {\n break;\n }\n }\n\n return $namespace !== '' ? $namespace : null;\n }\n\n \/**\n * @param array<mixed> $tokens\n * @return array<array{name: string, type: string, line: int}>\n *\/\n private function extractClasses(array $tokens): array\n {\n $classes = [];\n $count = count($tokens);\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if (!is_array($token)) {\n continue;\n }\n\n $type = match ($token[0]) {\n T_CLASS => 'class',\n T_INTERFACE => 'interface',\n T_TRAIT => 'trait',\n T_ENUM => 'enum',\n default => null,\n };\n\n if ($type === null) {\n continue;\n }\n\n \/\/ Prüfen ob es kein anonymes Class-Statement ist\n $prevIndex = $this->findPrevNonWhitespace($tokens, $i);\n if ($prevIndex !== null && is_array($tokens[$prevIndex]) && $tokens[$prevIndex][0] === T_NEW) {\n continue;\n }\n\n \/\/ Klassennamen finden\n for ($j = $i + 1; $j < $count; $j++) {\n if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {\n $classes[] = [\n 'name' => $tokens[$j][1],\n 'type' => $type,\n 'line' => $token[2],\n ];\n break;\n }\n }\n }\n\n return $classes;\n }\n\n \/**\n * @param array<mixed> $tokens\n * @return array<array{name: string, visibility: string|null, line: int}>\n *\/\n private function extractFunctions(array $tokens): array\n {\n $functions = [];\n $count = count($tokens);\n $braceDepth = 0;\n $inClass = false;\n\n for ($i = 0; $i < $count; $i++) {\n $token = $tokens[$i];\n\n if (!is_array($token)) {\n if ($token === '{') {\n $braceDepth++;\n } elseif ($token === '}') {\n $braceDepth--;\n if ($braceDepth === 0) {\n $inClass = false;\n }\n }\n continue;\n }\n\n \/\/ Klasse\/Interface\/Trait betreten\n if (in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], true)) {\n $inClass = true;\n }\n\n if ($token[0] !== T_FUNCTION) {\n continue;\n }\n\n \/\/ Visibility ermitteln\n $visibility = null;\n if ($inClass) {\n for ($j = $i - 1; $j >= 0; $j--) {\n if (!is_array($tokens[$j])) {\n break;\n }\n if ($tokens[$j][0] === T_PUBLIC) {\n $visibility = 'public';\n break;\n }\n if ($tokens[$j][0] === T_PROTECTED) {\n $visibility = 'protected';\n break;\n }\n if ($tokens[$j][0] === T_PRIVATE) {\n $visibility = 'private';\n break;\n }\n if ($tokens[$j][0] !== T_WHITESPACE && $tokens[$j][0] !== T_STATIC && $tokens[$j][0] !== T_FINAL && $tokens[$j][0] !== T_ABSTRACT) {\n break;\n }\n }\n }\n\n \/\/ Funktionsnamen finden\n for ($j = $i + 1; $j < $count; $j++) {\n if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) {\n $functions[] = [\n 'name' => $tokens[$j][1],\n 'visibility' => $visibility,\n 'line' => $token[2],\n ];\n break;\n }\n \/\/ Anonyme Funktion (kein Name)\n if ($tokens[$j] === '(') {\n break;\n }\n }\n }\n\n return $functions;\n }\n\n \/**\n * @param array<mixed> $tokens\n *\/\n private function findPrevNonWhitespace(array $tokens, int $index): ?int\n {\n for ($i = $index - 1; $i >= 0; $i--) {\n if (!is_array($tokens[$i]) || $tokens[$i][0] !== T_WHITESPACE) {\n return $i;\n }\n }\n\n return null;\n }\n}\n"
}
}