isSafeName($dataDir)) throw new \InvalidArgumentException("不正なキャッシュディレクトリ名: {$dataDir}"); $this->dataDir = rtrim($baseDir ?? ROOT.'/data/cache', '/').'/'.$dataDir.'/'; } /** * キャッシュの作成 * * @param string $type キャッシュ類(例:login、domainlist等) * @param array $data 保存するデータ * @return Result */ public function set(string $type, array $data): Result { if (!$this->isSafeName($type)) return Result::Error('エラー:不正なキャッシュ名。'); // 以前はdata/cache/が無いと作成に失敗した(再帰的ではなかった) if (!is_dir($this->dataDir) && !@mkdir($this->dataDir, 0755, true) && !is_dir($this->dataDir)) return Result::Error('エラー:キャッシュディレクトリの作成に失敗。'); $data['lifespan'] = time(); $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($json === false) return Result::Error('エラー:キャッシュデータのエンコードに失敗。'); if (file_put_contents("{$this->dataDir}{$type}.json", $json, LOCK_EX) === false) return Result::Error('エラー:キャッシュデータの保存に失敗。'); return Result::Success(); } /** * キャッシュから受け取り * * @param string $type キャッシュ類(例:login、domainlist等) * @return array */ public function get(string $type): array { if (!$this->isSafeName($type)) return []; if (!file_exists("{$this->dataDir}{$type}.json")) return []; $content = file_get_contents("{$this->dataDir}{$type}.json"); if ($content === false) return []; $data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) return []; return $data; } /** * キャッシュを破壊する * * @param string $name キャッシュのファイル名 * @return Result */ public function murder(string $name): Result { if (!$this->isSafeName($name)) return Result::Error('エラー:不正なキャッシュ名。'); if (!file_exists("{$this->dataDir}{$name}.json")) return Result::Error('キャッシュファイルが存在しない。'); if (!unlink("{$this->dataDir}{$name}.json")) return Result::Error('キャッシュファイルの削除に失敗。'); return Result::Success(); } /** * 名前が「$prefix」で始まるキャッシュを全部破壊する(関係するキャッシュをまとめて消す為) * * @param string $prefix キャッシュ名の始まり * @return int 削除した数 */ public function murderPrefix(string $prefix): int { if (!$this->isSafeName($prefix)) return 0; $n = 0; foreach (glob($this->dataDir.'*.json') ?: [] as $f) { if (str_starts_with(basename($f, '.json'), $prefix) && @unlink($f)) $n++; } return $n; } /** * キャッシュ有効期限の確認 * * @param array $cache * @param mixed $data デフォルト=null * @param string $dataPoint デフォルト=query * @return bool */ public function resurrect(array $cache, mixed $data = null, string $dataPoint = 'query'): bool { if (NULL === $data) return (!empty($cache) && (isset($cache['lifespan']) && time() < ($cache['lifespan'] + $this->dataDuration))); else return (!empty($cache) && (isset($cache[$dataPoint]) && $cache[$dataPoint] === $data) && (isset($cache['lifespan']) && time() < ($cache['lifespan'] + $this->dataDuration))); } /** * キャッシュの種類(data/cache/の下のディレクトリ)の一覧(無ければ空) * * @param string|null $baseDir 保存先(既定はdata/cache/、テスト用) * @return array 名前 */ public static function kinds(?string $baseDir = null): array { $base = rtrim($baseDir ?? ROOT.'/data/cache', '/'); $out = []; foreach (@scandir($base) ?: [] as $f) { if (preg_match('/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,199}$/', $f) && !str_contains($f, '..') && is_dir("{$base}/{$f}")) $out[] = $f; } return $out; } /** * このキャッシュのファイル * * @return array name(キャッシュ名)、size、mtime、created(作った時刻)、fresh(有効期限内か)(名前の順) */ public function entries(): array { $out = []; foreach (glob($this->dataDir.'*.json') ?: [] as $f) { $name = basename($f, '.json'); if (!$this->isSafeName($name)) continue; $data = $this->get($name); $out[] = ['name' => $name, 'size' => (int)@filesize($f), 'mtime' => (int)@filemtime($f), 'created' => isset($data['lifespan']) ? (int)$data['lifespan'] : null, 'fresh' => $this->resurrect($data)]; } usort($out, fn($a, $b) => strcmp($a['name'], $b['name'])); return $out; } /** * このキャッシュを全部破壊する(空になったディレクトリも消す) * * @return int 削除した数 */ public function murderAll(): int { $n = 0; foreach (glob($this->dataDir.'*.json') ?: [] as $f) if (@unlink($f)) $n++; @rmdir($this->dataDir); return $n; } /////// /** * キャッシュ名はファイル名になるので、英数字・「.」「_」「-」だけを許可する(「..」は不可) * * @param string $name キャッシュ名 * @return bool 安全ならtrue */ private function isSafeName(string $name): bool { return preg_match('/^[A-Za-z0-9._-]{1,200}$/', $name) === 1 && !str_contains($name, '..'); } }