Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Statische Prüfung der Cron-Handler: Der TaskContext wird ausschließlich als
* Parameter durchgereicht (es gibt keine Handler-Eigenschaft dafür). Eine
* Hilfsmethode, die `$ctx` benutzt, muss ihn deshalb selbst entgegennehmen –
* sonst ist die Variable im Methodenrumpf schlicht ungebunden und der Aufruf
* bricht erst zur Laufzeit ab, wenn eine Mail eintrifft.
*/
return static function (): void {
/**
* Sucht Methoden, die `$ctx` lesen, ohne ihn als Parameter zu deklarieren.
*
* Closures und Pfeilfunktionen bekommen keinen eigenen Geltungsbereich:
* Ihre Parameter und `use`-Übernahmen gelten als gebunden, damit übernommene
* Variablen nicht fälschlich als ungebunden gemeldet werden.
*
* @return string[] Fundstellen als "datei:zeile methode()"
*/
$scan = static function (string $dir): array {
$findings = [];
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
foreach ($files as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
$tokens = token_get_all(file_get_contents($file->getPathname()));
$count = count($tokens);
$method = null;
$bound = [];
for ($i = 0; $i < $count; $i++) {
$tok = $tokens[$i];
if (is_array($tok) && $tok[0] === T_FUNCTION) {
// Nächstes bedeutsames Token entscheidet: benannte Methode oder Closure
$j = $i + 1;
while ($j < $count && is_array($tokens[$j]) && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT], true)) {
$j++;
}
$named = is_array($tokens[$j]) && $tokens[$j][0] === T_STRING;
$open = $j;
while ($open < $count && $tokens[$open] !== '(') {
$open++;
}
$params = [];
$depth = 0;
for ($k = $open; $k < $count; $k++) {
if ($tokens[$k] === '(') {
$depth++;
} elseif ($tokens[$k] === ')') {
if (--$depth === 0) {
break;
}
} elseif (is_array($tokens[$k]) && $tokens[$k][0] === T_VARIABLE) {
$params[$tokens[$k][1]] = true;
}
}
// use (...) einer Closure hängt direkt hinter der Parameterliste
$u = $k + 1;
while ($u < $count && is_array($tokens[$u]) && in_array($tokens[$u][0], [T_WHITESPACE, T_COMMENT], true)) {
$u++;
}
if (is_array($tokens[$u]) && $tokens[$u][0] === T_USE) {
$depth = 0;
for ($k = $u; $k < $count; $k++) {
if ($tokens[$k] === '(') {
$depth++;
} elseif ($tokens[$k] === ')') {
if (--$depth === 0) {
break;
}
} elseif (is_array($tokens[$k]) && $tokens[$k][0] === T_VARIABLE) {
$params[$tokens[$k][1]] = true;
}
}
}
if ($named) {
$method = $tokens[$j][1];
$bound = $params;
} else {
$bound += $params;
}
$i = $k;
continue;
}
if (is_array($tok) && $tok[0] === T_VARIABLE && $tok[1] === '$ctx'
&& $method !== null && !isset($bound['$ctx'])) {
$findings[] = $file->getPathname() . ':' . $tok[2] . ' ' . $method . '()';
}
}
}
sort($findings);
return $findings;
};
test('Cron-Handler reichen den TaskContext als Parameter durch', function () use ($scan) {
$findings = $scan(dirname(__DIR__) . '/src/Cron');
assertSame([], $findings, 'Ungebundenes $ctx: ' . implode(', ', $findings));
});
test('Die Prüfung erkennt eine Methode ohne $ctx-Parameter', function () use ($scan) {
$tmp = sys_get_temp_dir() . '/opennit-ctx-' . getmypid();
@mkdir($tmp, 0700, true);
file_put_contents($tmp . '/Probe.php', <<<'PHP'
<?php
final class Probe
{
public function execute($ctx): void { $this->helfer($ctx); }
private function helfer($manager): void { $this->wert($ctx); }
}
PHP);
$findings = $scan($tmp);
assertSame(1, count($findings));
assertTrue(str_contains($findings[0], 'helfer()'));
unlink($tmp . '/Probe.php');
rmdir($tmp);
});
};