Backup #1034

ID1034
Dateipfad/var/www/dev.campus.systemische-tools.de/src/Infrastructure/CodeAnalysis/PhpFileParser.php
Version7
Typ modified
Größe18.9 KB
Hash2fb565a81aac06e89e7bf1d272b14fd4620b53d27634e13a9da5c9256f9ecf91
Datum2025-12-25 01:48:30
Geändert vonclaude-code-hook
GrundClaude 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
{
    use TokenNavigatorTrait;
    use UseStatementExtractorTrait;

    /** @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 {

... (172 weitere Zeilen)

Vollständig herunterladen

Aktionen

Herunterladen

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