Backup #1031
| ID | 1031 |
| Dateipfad | /var/www/dev.campus.systemische-tools.de/src/Infrastructure/CodeAnalysis/PhpFileParser.php |
| Version | 4 |
| Typ |
modified |
| Größe | 20.9 KB |
| Hash | 10654a1305d1c14617f15fe103654ffb2f4556c3916ee3af2485b274f2ab7a96 |
| Datum | 2025-12-25 01:44:54 |
| Geändert von | claude-code-hook |
| Grund | Claude Code Pre-Hook Backup vor Edit-Operation |
| Datei existiert |
Ja
|
Dateiinhalt
<?php
declare(strict_types=1);
namespace Infrastructure\CodeAnalysis;
// @responsibility: PHP-Datei-Strukturanalyse via Tokenizer
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 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,
];
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<mixed> $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;
}
/**
* 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
{
$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<mixed> $tokens
* @return array<array{name: string, visibility: string|null, line: int}>
*/
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 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;
... (260 weitere Zeilen)
Vollständig herunterladen
Aktionen
Andere Versionen dieser Datei
| ID |
Version |
Typ |
Größe |
Datum |
| 2021 |
12 |
modified |
8.0 KB |
2025-12-28 23:20 |
| 1039 |
11 |
modified |
11.7 KB |
2025-12-25 01:51 |
| 1038 |
10 |
modified |
11.7 KB |
2025-12-25 01:51 |
| 1037 |
9 |
modified |
15.2 KB |
2025-12-25 01:50 |
| 1036 |
8 |
modified |
15.2 KB |
2025-12-25 01:50 |
| 1034 |
7 |
modified |
18.9 KB |
2025-12-25 01:48 |
| 1033 |
6 |
modified |
18.8 KB |
2025-12-25 01:48 |
| 1032 |
5 |
modified |
20.9 KB |
2025-12-25 01:45 |
| 1031 |
4 |
modified |
20.9 KB |
2025-12-25 01:44 |
| 1028 |
3 |
modified |
20.9 KB |
2025-12-25 00:18 |
| 1027 |
2 |
modified |
20.9 KB |
2025-12-25 00:17 |
| 878 |
1 |
modified |
6.6 KB |
2025-12-23 14:14 |
← Zurück zur Übersicht