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 inkl. Dependencies. * * @return array{ * namespace: string|null, * classes: array, * functions: array, * uses: array, * extends_class: string|null, * implements_interfaces: array, * traits_used: array, * constructor_deps: array, * 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, ]; if (!file_exists($filePath) || !is_readable($filePath)) { $result['error'] = 'Datei nicht lesbar'; return $result; } $content = file_get_contents($filePath); if ($content === false) { $result['error'] = 'Datei konnte nicht gelesen werden'; return $result; } try { $tokens = @token_get_all($content); } catch (\Throwable $e) { $result['error'] = 'Token-Fehler: ' . $e->getMessage(); 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; } /** * @param array $tokens */ private function extractNamespace(array $tokens): ?string { $namespace = ''; $capturing = false; foreach ($tokens as $token) { if (is_array($token)) { if ($token[0] === T_NAMESPACE) { $capturing = true; continue; } if ($capturing) { if ($token[0] === T_NAME_QUALIFIED || $token[0] === T_STRING) { $namespace .= $token[1]; } elseif ($token[0] === T_NS_SEPARATOR) { $namespace .= '\\'; } } } elseif ($capturing && ($token === ';' || $token === '{')) { break; } } return $namespace !== '' ? $namespace : null; } /** * @param array $tokens * @return array */ private function extractClasses(array $tokens): array { $classes = []; $count = count($tokens); for ($i = 0; $i < $count; $i++) { $token = $tokens[$i]; if (!is_array($token)) { continue; } $type = match ($token[0]) { T_CLASS => 'class', T_INTERFACE => 'interface', T_TRAIT => 'trait', T_ENUM => 'enum', default => null, }; if ($type === null) { continue; } // Skip anonymous class statements $prevIndex = $this->findPrevNonWhitespace($tokens, $i); if ($prevIndex !== null && is_array($tokens[$prevIndex]) && $tokens[$prevIndex][0] === T_NEW) { continue; } // Find class name for ($j = $i + 1; $j < $count; $j++) { if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) { $classes[] = [ 'name' => $tokens[$j][1], 'type' => $type, 'line' => $token[2], ]; break; } } } return $classes; } /** * @param array $tokens * @return array */ private function extractFunctions(array $tokens): array { $functions = []; $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_INTERFACE, T_TRAIT, T_ENUM], true)) { $inClass = true; } if ($token[0] !== T_FUNCTION) { continue; } $visibility = null; if ($inClass) { for ($j = $i - 1; $j >= 0; $j--) { if (!is_array($tokens[$j])) { break; } 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) { break; } } } for ($j = $i + 1; $j < $count; $j++) { if (is_array($tokens[$j]) && $tokens[$j][0] === T_STRING) { $functions[] = [ 'name' => $tokens[$j][1], 'visibility' => $visibility, 'line' => $token[2], ]; break; } if ($tokens[$j] === '(') { break; } } } return $functions; } /** * Extract constructor parameter type hints (DI dependencies). * * @param array $tokens * @return array */ 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); } }