$roots * @param list $excludedAbsolutePaths * @return list */ public static function phpFiles(array $roots, array $excludedAbsolutePaths = []): array { $files = []; $excluded = array_fill_keys(array_map(self::normalizePath(...), $excludedAbsolutePaths), true); foreach ($roots as $root) { $root = self::normalizePath($root); if (! is_dir($root)) { continue; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS) ); /** @var SplFileInfo $file */ foreach ($iterator as $file) { if (! $file->isFile()) { continue; } $path = self::normalizePath($file->getPathname()); if (pathinfo($path, PATHINFO_EXTENSION) !== 'php') { continue; } if (isset($excluded[$path])) { continue; } $files[] = $path; } } sort($files); return array_values(array_unique($files)); } public static function projectRoot(): string { return self::normalizePath(dirname(__DIR__, 3)); } public static function relativePath(string $absolutePath): string { $absolutePath = self::normalizePath($absolutePath); $root = self::projectRoot(); if (str_starts_with($absolutePath, $root.'/')) { return substr($absolutePath, strlen($root) + 1); } return $absolutePath; } public static function read(string $path): string { return (string) file_get_contents($path); } public static function snippet(string $source, int $line, int $contextLines = 2): string { $allLines = preg_split('/\R/', $source) ?: []; $line = max(1, $line); $start = max(1, $line - $contextLines); $end = min(count($allLines), $line + $contextLines); $snippet = []; for ($index = $start; $index <= $end; $index++) { $prefix = $index === $line ? '>' : ' '; $snippet[] = sprintf('%s%4d | %s', $prefix, $index, $allLines[$index - 1] ?? ''); } return implode("\n", $snippet); } private static function normalizePath(string $path): string { return str_replace('\\', '/', $path); } }