list() as $f) echo $f['name'], ' ', $f['size'], "\n"; * $tail = $logs->tail('db_log.txt', 100, 'エラー'); */ class Logs { private string $dir; /** * @param string|null $dir ログのディレクトリ(既定はROOT/log、テスト用) */ public function __construct(?string $dir = null) { $this->dir = rtrim($dir ?? ROOT.'/log', '/\\'); } public function dir(): string { return $this->dir; } /** * ログのファイルの一覧(ディレクトリが無ければ空) * * @return array name、size、mtime(名前の順) */ public function list(): array { $out = []; foreach (@scandir($this->dir) ?: [] as $f) { $p = $this->path($f); if ($p === null) continue; $out[] = ['name' => $f, 'size' => (int)filesize($p), 'mtime' => (int)filemtime($p)]; } return $out; } /** * ログのファイルのパス(ディレクトリの中の普通のファイルだけ) * * @param string $name ファイル名 * @return string|null 無い・不正な名前ならnull */ public function path(string $name): ?string { if (!preg_match('/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,200}$/', $name) || str_contains($name, '..')) return null; $p = $this->dir.'/'.$name; return is_file($p) && !is_link($p) ? $p : null; } /** * 最後の行(新しい順ではなく、ファイルの順) * * @param string $name ファイル名 * @param int $lines 行数 * @param string $search 含む行だけ(大文字・小文字を区別しない、空なら全部) * @return array|null lines(行の配列)、more(前にもっと有るか)。ファイルが無ければnull */ public function tail(string $name, int $lines = 200, string $search = ''): ?array { $p = $this->path($name); if ($p === null) return null; $lines = max(1, min($lines, 10000)); $fh = @fopen($p, 'rb'); if (!$fh) return null; try { if ($search !== '') { // 検索は全部を読む(一致した最後の行だけ残す) $out = []; $more = false; while (($l = fgets($fh)) !== false) { $l = rtrim($l, "\r\n"); if (mb_stripos($l, $search, 0, 'UTF-8') === false) continue; $out[] = $l; if (count($out) > $lines) { array_shift($out); $more = true; } } return ['lines' => $out, 'more' => $more]; } // 後ろから読む $size = (int)fstat($fh)['size']; $pos = $size; $buf = ''; $chunk = 65536; while ($pos > 0 && substr_count($buf, "\n") <= $lines) { $n = min($chunk, $pos); $pos -= $n; fseek($fh, $pos); $buf = (string)fread($fh, $n).$buf; } $all = explode("\n", rtrim($buf, "\n")); if ($buf === '') $all = []; $more = $pos > 0 || count($all) > $lines; if (count($all) > $lines) $all = array_slice($all, -$lines); return ['lines' => array_map(fn($l) => rtrim($l, "\r"), $all), 'more' => $more]; } finally { fclose($fh); } } /** * ログを空にする */ public function clear(string $name): bool { $p = $this->path($name); return $p !== null && @file_put_contents($p, '', LOCK_EX) !== false; } /** * ログのファイルを消す */ public function delete(string $name): bool { $p = $this->path($name); return $p !== null && @unlink($p); } }