← Code-Übersicht

CodeDateiScanner.php

Pfad: src/Infrastructure/Code/CodeDateiScanner.php

Ext: php

Größe: 4536 Bytes

Geändert: 2026-07-16 10:05:26+02

<?php
declare(strict_types=1);

namespace Demo\Infrastructure\Code;

use Demo\Domain\Code\CodeDatei;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;

/**
 * Scannt das Projektverzeichnis nach Code-Dateien fuer den Mirror.
 */
final class CodeDateiScanner
{
    /** @var string Root des Projekts. */
    private string $projektRoot;

    /** @var list<string> Dateiendungen, die als Code betrachtet werden. */
    private array $erlaubteErweiterungen;

    /** @var list<string> Auszuschliessende Ordnernamen (Komponenten). */
    private array $ignorierteOrdner;

    /**
     * @param string       $projektRoot            Projekt-Root.
     * @param list<string> $erlaubteErweiterungen  Endungen als Code.
     * @param list<string> $ignorierteOrdner       Ignorierte Ordner.
     */
    public function __construct(
        string $projektRoot,
        array $erlaubteErweiterungen = ['php', 'js', 'css', 'sql', 'sh', 'yml', 'yaml', 'json'],
        array $ignorierteOrdner = ['.git', 'credentials', 'node_modules', 'vendor', 'tmp']
    ) {
        $this->projektRoot = rtrim($projektRoot, '/');
        $this->erlaubteErweiterungen = $erlaubteErweiterungen;
        $this->ignorierteOrdner = $ignorierteOrdner;
    }

    /**
     * Scannt alle erlaubten Dateien.
     *
     * @return list<CodeDatei> Dateien mit Inhalt, sortiert nach Pfad.
     */
    public function scan(): array
    {
        $dateien = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->projektRoot, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $eintrag) {
            if (!$eintrag->isFile()) {
                continue;
            }
            $pfad = $eintrag->getPathname();
            if (!$this->istCodeDatei($eintrag)) {
                continue;
            }
            if ($this->istIgnoriert($pfad)) {
                continue;
            }
            $inhalt = file_get_contents($pfad);
            if ($inhalt === false) {
                continue;
            }
            $dateien[] = $this->baueCodeDatei($eintrag, (string) $inhalt);
        }

        usort($dateien, static fn (CodeDatei $a, CodeDatei $b): int => strcmp($a->relativerPfad, $b->relativerPfad));

        return $dateien;
    }

    /**
     * Erzeugt eine CodeDatei aus einem Dateieintrag.
     *
     * @param SplFileInfo $datei  Metadaten aus dem FS.
     * @param string      $code   Dateicontent.
     * @return CodeDatei Datensatz fuer den Spiegel.
     */
    private function baueCodeDatei(SplFileInfo $datei, string $code): CodeDatei
    {
        $relativerPfad = $this->relativePfad($datei->getPathname());
        $groesse = (int) $datei->getSize();
        return new CodeDatei(
            0,
            $relativerPfad,
            $datei->getFilename(),
            strtolower($datei->getExtension()),
            $code,
            hash('sha256', $code),
            $groesse,
            gmdate('c', $datei->getMTime()),
            gmdate('c', $datei->getCTime())
        );
    }

    /**
     * Prüft Extension + Dateigroesse + Lesbarkeit.
     *
     * @param SplFileInfo $datei Eintrag.
     * @return bool true bei beruecksichtigungsfaehig.
     */
    private function istCodeDatei(SplFileInfo $datei): bool
    {
        $groesse = (int) $datei->getSize();
        if ($groesse > 2_000_000) {
            return false;
        }
        $ext = strtolower($datei->getExtension());
        return in_array($ext, $this->erlaubteErweiterungen, true);
    }

    /**
     * Prueft, ob der absolute Pfad einen ausgeschlossenen Ordner enthaelt.
     *
     * @param string $pfad Absoluter Dateipfad.
     * @return bool true wenn ignoriert.
     */
    private function istIgnoriert(string $pfad): bool
    {
        $norm = str_replace('\\', '/', $pfad);
        $teile = explode('/', str_replace($this->projektRoot . '/', '', $norm));
        foreach ($teile as $teil) {
            if (in_array($teil, $this->ignorierteOrdner, true)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Liefert den relativen Pfad zum Projekt-Root.
     *
     * @param string $absoluterPfad Voller Dateipfad.
     * @return string Relativer Pfad.
     */
    private function relativePfad(string $absoluterPfad): string
    {
        return ltrim(str_replace($this->projektRoot . '/', '', str_replace('\\', '/', $absoluterPfad)), '/');
    }
}