OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

ControllerMethodCallsTest.php

4 KB
<?php

declare(strict_types=1);

/**
 * Deckt ab, dass Controller nur eigene Methoden aufrufen, die es auch gibt.
 *
 * `php -l` prüft die Syntax, nicht die Auflösung von Methoden: Ein Aufruf auf
 * eine nicht vorhandene Methode fällt erst beim Seitenaufruf auf und endet dort
 * als interner Fehler. Diese Prüfung zieht den Befund auf die Testzeit vor.
 */
return static function (): void {

    /** @return list<string> Alle Controller-Dateien */
    $dateien = static function (): array {
        $liste = [];
        $lauf  = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator(
                dirname(__DIR__) . '/src/Controllers',
                FilesystemIterator::SKIP_DOTS
            )
        );
        foreach ($lauf as $datei) {
            if ($datei->isFile() && $datei->getExtension() === 'php') {
                $liste[] = $datei->getPathname();
            }
        }
        sort($liste);
        return $liste;
    };

    /** Voll qualifizierter Klassenname einer Datei. */
    $klasseVon = static function (string $datei): ?string {
        $inhalt = (string)file_get_contents($datei);
        if (!preg_match('/^namespace\s+([^;]+);/m', $inhalt, $ns)) {
            return null;
        }
        if (!preg_match('/^(?:final\s+|abstract\s+)?class\s+(\w+)/m', $inhalt, $cl)) {
            return null;
        }
        return trim($ns[1]) . '\\' . $cl[1];
    };

    /**
     * Alle auf $this aufgerufenen Methodennamen einer Datei.
     * Aufrufe auf andere Objekte bleiben außen vor – deren Klasse steht hier
     * nicht fest.
     *
     * @return list<string>
     */
    $aufrufe = static function (string $datei): array {
        $inhalt = (string)file_get_contents($datei);
        preg_match_all('/\$this->([a-zA-Z_]\w*)\s*\(/', $inhalt, $m);
        return array_values(array_unique($m[1]));
    };

    // ----------------------------------------------------------------
    // Die Prüfung
    // ----------------------------------------------------------------

    test('Jeder Controller-Aufruf auf $this trifft eine vorhandene Methode', function () use ($dateien, $klasseVon, $aufrufe) {
        $geprueft = 0;
        $fehlend  = [];

        foreach ($dateien() as $datei) {
            $klasse = $klasseVon($datei);
            if ($klasse === null || !class_exists($klasse)) {
                continue;
            }
            $reflex = new ReflectionClass($klasse);
            if ($reflex->isAbstract()) {
                continue;
            }
            // Klassen mit __call lösen Methoden zur Laufzeit auf.
            if ($reflex->hasMethod('__call')) {
                continue;
            }

            foreach ($aufrufe($datei) as $name) {
                $geprueft++;
                if (!$reflex->hasMethod($name)) {
                    $fehlend[] = $reflex->getShortName() . '::' . $name . '()';
                }
            }
        }

        assertTrue($geprueft > 200, 'Zu wenige Aufrufe geprüft – die Sammlung greift nicht');
        assertSame([], $fehlend, 'Nicht vorhandene Methoden: ' . implode(', ', $fehlend));
    });

    // ----------------------------------------------------------------
    // Der konkrete Fall
    // ----------------------------------------------------------------

    test('Die Budget-Einstellungen können ihren Wert lesen und schreiben', function () {
        // Die Seite liest den Standard-Steuersatz und schreibt ihn zurück;
        // beides läuft über system_settings.
        $klasse = new ReflectionClass(\OpenNIT\Controllers\Admin\BudgetAdminController::class);

        foreach (['getSetting', 'saveSetting'] as $name) {
            assertTrue($klasse->hasMethod($name), 'Fehlt: ' . $name);
        }

        $src = (string)file_get_contents(
            dirname(__DIR__) . '/src/Controllers/Admin/BudgetAdminController.php'
        );
        assertTrue(str_contains($src, 'SystemSetting($this->db())'), 'Die Ablage weicht vom üblichen Weg ab');
    });
};