PhpFileParser.php

Code Hygiene Score: 94

Keine Issues gefunden.

Dependencies 4

Klassen 1

Funktionen 5

Verwendet von 2

Versionen 12

Code

<?php

declare(strict_types=1);

namespace Infrastructure\CodeAnalysis;

/**
 * @responsibility PHP-Datei-Strukturanalyse via Tokenizer
 *
 * @composition Nutzt Mixin-Pattern für Separation of Concerns:
 *   - TokenNavigatorTrait: Token-Stream Navigation und Positionierung
 *   - UseStatementExtractorTrait: use-Statement und Import-Map Extraktion
 *   - InheritanceExtractorTrait: extends/implements/use-Trait Analyse
 *   - ClassFunctionExtractorTrait: Methoden, Properties und Constructor-Dependencies
 *
 * @see TokenNavigatorTrait
 * @see UseStatementExtractorTrait
 * @see InheritanceExtractorTrait
 * @see ClassFunctionExtractorTrait
 */
final class PhpFileParser
{
    use TokenNavigatorTrait;
    use UseStatementExtractorTrait;
    use InheritanceExtractorTrait;
    use ClassFunctionExtractorTrait;

    /** @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;
    }

    /**
     * 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);
    }
}
← Übersicht Graph