> */ public function all(string $username): array { $data = $this->read($username); return array_values($data['rules'] ?? []); } /** @return array|null */ public function find(string $username, string $id): ?array { foreach ($this->all($username) as $rule) { if (($rule['id'] ?? '') === $id) { return $rule; } } return null; } /** @param array $rule @return array */ public function create(string $username, array $rule): array { $data = $this->read($username); $now = time(); $rule['id'] = bin2hex(random_bytes(12)); $rule['created_at'] = $now; $rule['updated_at'] = $now; $data['rules'][] = $rule; $this->write($username, $data); return $rule; } /** @param array $patch @return array */ public function update(string $username, string $id, array $patch): array { $data = $this->read($username); foreach ($data['rules'] as $i => $rule) { if (($rule['id'] ?? '') === $id) { $updated = array_merge($rule, $patch, ['id' => $id, 'updated_at' => time()]); $data['rules'][$i] = $updated; $this->write($username, $data); return $updated; } } throw new RuntimeException('Rule not found'); } public function delete(string $username, string $id): void { $data = $this->read($username); $before = count($data['rules']); $data['rules'] = array_values(array_filter($data['rules'], static fn (array $rule): bool => ($rule['id'] ?? '') !== $id)); if (count($data['rules']) === $before) { throw new RuntimeException('Rule not found'); } $this->write($username, $data); } /** @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 { $this->assertUsername($username); return rtrim($this->baseDir, '/') . '/users/' . $username . '/rules.json'; } private function assertUsername(string $username): void { if (!preg_match('/^[A-Za-z0-9._-]+$/', $username)) { throw new InvalidArgumentException('Invalid username'); } } }