int (only for list_rows), 'name' => string, 'message' => string, 'email' => ?string, 'website' => ?string, 'timestamp' => string (or DateTimeInterface for list_rows)] // message is pre-escaped at insert time. // timestamp is DateTimeInterface::ISO8601_EXPANDED format. abstract class Database { public function append_row(string $name, string $message, ?string $email = NULL, ?string $website = NULL) { $timestamp = date(DATE_ISO8601_EXPANDED); $this->_append_row(compact('name', 'message', 'email', 'website', 'timestamp')); } public function list_rows(): array { $data = $this->_list_rows(); foreach($data as &$row) { $row['timestamp'] = DateTime::createFromFormat(DATE_ISO8601_EXPANDED, $row['timestamp']); } return $data; } public function delete_row(int $id): bool { return $this->_delete_row($id); } abstract public function __construct(string $file); abstract protected function _append_row(array $db_row); abstract protected function _list_rows(): array; abstract protected function _delete_row(int $id): bool; } // sigh, refactor later, it wont be a perf issue for any realistic number of entries. abstract class FileDatabase extends Database { protected string $file; protected array $data; public function __construct(string $file) { if (file_exists($file)) $this->data = $this->load($file); else $this->data = array(); $this->file = $file; } protected function _append_row(array $db_row) { array_unshift($this->data, array(...$db_row, 'id' => max(array_map(fn($a) => $a['id'], $this->data)))); $this->save(); } protected function _list_rows(): array { $data = $this->data; return $data; } protected function _delete_row(int $id): bool { foreach(array_keys($this->data) as $key) if ($this->data[$key]['id'] === $id) { unset($this->data[$key]); $this->save(); return true; } return false; } abstract protected function save(); abstract protected function load(string $file): array; } // notes: `id` is not stable final class JsonDatabase extends FileDatabase { protected function save() { file_put_contents($this->file, json_encode($this->data)); } protected function load(string $file): array { return json_decode(file_get_contents($file), associative: true); } } // notes: `id` is not stable final class JsonlDatabase extends FileDatabase { protected function save() { $fp = fopen($this->file, 'w'); foreach($data as $row) { fwrite($fp, json_encode($row)."\n"); } fclose($fp); } protected function load(string $file): array { return array_map(fn($s) => json_decode($s, associative: true), file($file)); } } // notes: `id` is not stable final class CsvDatabase extends FileDatabase { private const array KEY_ORDER = ['name', 'message', 'email', 'website', 'timestamp']; protected function save() { $fp = fopen($this->file, 'w'); foreach($this->data as $row) { fputcsv($fp, array_map(fn($k) => $row[$k]??'', self::KEY_ORDER)); } fclose($fp); } protected function load(string $file): array { return array_map(fn($s) => array_combine(self::KEY_ORDER, str_getcsv($s)), file($file)); } } final class SqliteDatabase extends Database { private Sqlite3 $handle; public function __construct(string $file) { $this->handle = new Sqlite3($file); if (!$this->handle->querySingle('SELECT count(*) FROM sqlite_master')) $this->handle->exec('CREATE TABLE visitors (id INTEGER PRIMARY KEY, name TEXT NOT NULL, message TEXT NOT NULL, email TEXT, website TEXT, timestamp TEXT NOT NULL)'); } protected function _append_row(array $db_row) { $stmt = $this->handle->prepare('INSERT INTO visitors (name, message, email, website, timestamp) VALUES (:name, :message, :email, :website, :timestamp)'); foreach($db_row as $key => $value) $stmt->bindValue($key, $value); $stmt->execute(); } protected function _list_rows(): array { $rows = array(); $res = $this->handle->query('SELECT * FROM visitors ORDER BY id DESC'); while ($row = $res->fetchArray(SQLITE3_ASSOC)) $rows[] = $row; return $rows; } protected function _delete_row(int $id): bool { $stmt = $this->handle->prepare('SELECT count(*) FROM visitors WHERE id=:id'); $stmt->bindValue('id', $id); if (!$stmt->execute()->fetchArray(SQLITE3_NUM)[0]) return false;; $stmt = $this->handle->prepare('DELETE FROM visitors WHERE id=:id'); $stmt->bindValue('id', $id); $stmt->execute(); return true; } }