Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Deckt ab, dass keine Vorlage aus einer statischen Funktion heraus auf die
* Vorlagen-Helfer zugreift.
*
* Eine mit `static` deklarierte Funktion hat kein `$this`. Der Zugriff fällt
* erst beim Ausliefern der Seite auf – und bricht sie dann ganz ab.
*/
return static function (): void {
/** Alle Vorlagen, Pfad relativ zum Projekt. */
$dateien = static function (): array {
$wurzel = dirname(__DIR__) . '/templates';
$alle = [];
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($wurzel));
foreach ($it as $datei) {
if ($datei->isFile() && $datei->getExtension() === 'php') {
$alle[] = $datei->getPathname();
}
}
sort($alle);
return $alle;
};
/**
* Sucht statische Funktionen, deren Rumpf `$this` verwendet.
*
* Gearbeitet wird auf den Token des Quelltexts: Nur so lassen sich die
* Grenzen einer Funktion sicher bestimmen – ein Suchmuster über den Text
* greift entweder zu weit oder zu kurz.
*
* @return list<int> Zeilennummern der Fundstellen.
*/
$fundstellen = static function (string $quelle): array {
$token = token_get_all($quelle);
$treffer = [];
$anzahl = count($token);
for ($i = 0; $i < $anzahl; $i++) {
if (!is_array($token[$i]) || $token[$i][0] !== T_STATIC) {
continue;
}
// Nächstes bedeutungstragendes Token: function oder fn?
$j = $i + 1;
while ($j < $anzahl && is_array($token[$j]) && in_array($token[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
$j++;
}
if (!is_array($token[$j] ?? null) || !in_array($token[$j][0], [T_FUNCTION, T_FN], true)) {
continue;
}
$istPfeil = $token[$j][0] === T_FN;
$zeile = $token[$j][2];
// Rumpf abgrenzen: beim Pfeil bis zum Ende des Ausdrucks,
// sonst über die geschweiften Klammern.
$tiefe = 0;
$start = $j;
$ende = $anzahl - 1;
$imRumpf = false;
for ($k = $j; $k < $anzahl; $k++) {
$t = $token[$k];
$text = is_array($t) ? $t[1] : $t;
if (in_array($text, ['(', '[', '{'], true)) {
$tiefe++;
} elseif (in_array($text, [')', ']', '}'], true)) {
$tiefe--;
if (!$istPfeil && $imRumpf && $tiefe === 0) {
$ende = $k;
break;
}
} elseif ($text === '{' || (is_array($t) && $t[0] === T_CURLY_OPEN)) {
$imRumpf = true;
}
if (!$istPfeil && $text === '{' && !$imRumpf) {
$imRumpf = true;
}
if ($istPfeil && $tiefe <= 0 && in_array($text, [';', ','], true) && $k > $j + 2) {
$ende = $k;
break;
}
}
for ($k = $start; $k <= $ende; $k++) {
$t = $token[$k];
if (is_array($t) && $t[0] === T_VARIABLE && $t[1] === '$this') {
$treffer[] = $zeile;
break;
}
}
}
return $treffer;
};
test('Keine statische Funktion einer Vorlage greift auf $this zu', function () use ($dateien, $fundstellen) {
$wurzel = dirname(__DIR__) . '/';
$fehler = [];
foreach ($dateien() as $datei) {
foreach ($fundstellen((string)file_get_contents($datei)) as $zeile) {
$fehler[] = str_replace($wurzel, '', $datei) . ':' . $zeile;
}
}
assertSame([], $fehler, 'Zur Laufzeit ohne $this: ' . implode(', ', $fehler));
});
test('Der Suchlauf erkennt eine statische Funktion mit $this', function () use ($fundstellen) {
// Sonst wäre der Test oben immer grün.
assertSame([2], $fundstellen("<?php\n\$f = static fn(\$v) => \$this->money(\$v);\n"));
assertSame([2], $fundstellen("<?php\n\$f = static function (\$v) {\n return \$this->money(\$v);\n};\n"));
assertSame([], $fundstellen("<?php\n\$f = fn(\$v) => \$this->money(\$v);\n"));
assertSame([], $fundstellen("<?php\n\$f = static fn(\$v) => \$v * 2;\n\$g = \$this->money(1);\n"));
});
test('Der Suchlauf erfasst ueberhaupt Vorlagen', function () use ($dateien) {
assertTrue(count($dateien()) > 200, 'Die Vorlagen konnten nicht gelesen werden');
});
};