> $entries */ public function replaceRuleEntries(string $username, string $ruleId, array $entries): void { $data = $this->read($username); $data['rules'][$ruleId] = array_values($entries); $this->write($username, $data); } /** @return array> */ public function entriesForRule(string $username, string $ruleId): array { $data = $this->read($username); return array_values($data['rules'][$ruleId] ?? []); } /** @return array>> */ public function allRuleEntries(string $username): array { return $this->read($username)['rules']; } /** @return array> */ public function deleteRuleEntries(string $username, string $ruleId): array { $data = $this->read($username); $entries = array_values($data['rules'][$ruleId] ?? []); unset($data['rules'][$ruleId]); $this->write($username, $data); return $entries; } /** @return array{rules:array>>} */ private function read(string $username): array { $path = $this->path($username); if (!is_file($path)) { return ['rules' => []]; } $data = json_decode((string)file_get_contents($path), true); return is_array($data) && isset($data['rules']) && is_array($data['rules']) ? $data : ['rules' => []]; } /** @param array{rules:array>>} $data */ private function write(string $username, array $data): void { $path = $this->path($username); $dir = dirname($path); if (!is_dir($dir)) { mkdir($dir, 0700, true); } $tmp = $path . '.tmp.' . getmypid(); file_put_contents($tmp, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX); @chmod($tmp, 0600); rename($tmp, $path); } private function path(string $username): string { if (!preg_match('/^[A-Za-z0-9._-]+$/', $username)) { throw new InvalidArgumentException('Invalid username'); } return rtrim($this->baseDir, '/') . '/users/' . $username . '/da-sync.json'; } }