postgres database implementation

This commit is contained in:
2026-05-22 19:07:03 +02:00
parent dacc9ce2bc
commit e7f35d71e3
2 changed files with 146 additions and 97 deletions
+38 -9
View File
@@ -34,13 +34,42 @@ function load_env() {
// Automatically load on include
load_env();
function get_firebase_config(): array {
return [
'apiKey' => getenv('FIREBASE_API_KEY') ?: '',
'authDomain' => getenv('FIREBASE_AUTH_DOMAIN') ?: '',
'projectId' => getenv('FIREBASE_PROJECT_ID') ?: '',
'storageBucket' => getenv('FIREBASE_STORAGE_BUCKET') ?: '',
'messagingSenderId' => getenv('FIREBASE_MESSAGING_SENDER_ID') ?: '',
'appId' => getenv('FIREBASE_APP_ID') ?: '',
];
/**
* Get a PDO connection to the database.
*/
function get_db_connection(): ?PDO {
static $pdo = null;
if ($pdo !== null) {
return $pdo;
}
$url = getenv('DATABASE_URL');
if (!$url) {
return null;
}
$parsedUrl = parse_url($url);
if ($parsedUrl === false || !isset($parsedUrl['host'], $parsedUrl['user'], $parsedUrl['pass'], $parsedUrl['path'])) {
return null;
}
$host = $parsedUrl['host'];
$port = $parsedUrl['port'] ?? 5432;
$user = $parsedUrl['user'];
$pass = $parsedUrl['pass'];
$db = ltrim($parsedUrl['path'], '/');
$dsn = "pgsql:host=$host;port=$port;dbname=$db";
try {
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
return $pdo;
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
return null;
}
}
+100 -80
View File
@@ -7,54 +7,72 @@ if (session_status() === PHP_SESSION_NONE) {
require_once __DIR__ . '/config.php';
function decode_firestore_value($value) {
if (!is_array($value)) return $value;
if (isset($value['stringValue'])) return $value['stringValue'];
if (isset($value['integerValue'])) return (int)$value['integerValue'];
if (isset($value['doubleValue'])) return (float)$value['doubleValue'];
if (isset($value['booleanValue'])) return (bool)$value['booleanValue'];
if (isset($value['nullValue'])) return null;
if (isset($value['arrayValue']['values'])) {
return array_map('decode_firestore_value', $value['arrayValue']['values']);
}
if (isset($value['mapValue']['fields'])) {
return array_map('decode_firestore_value', $value['mapValue']['fields']);
}
return $value;
}
function init_db() {
$pdo = get_db_connection();
if (!$pdo) return;
function decode_firestore_document(array $doc): array {
if (!isset($doc['fields'])) return [];
return array_map('decode_firestore_value', $doc['fields']);
try {
$pdo->exec("CREATE TABLE IF NOT EXISTS recipes (
slug VARCHAR(255) PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
$stmt = $pdo->query("SELECT COUNT(*) FROM recipes");
if ($stmt && $stmt->fetchColumn() == 0) {
$path = __DIR__ . '/data/recipes.json';
if (file_exists($path)) {
$json = file_get_contents($path);
$data = json_decode($json, true);
if (is_array($data) && count($data) > 0) {
$insert = $pdo->prepare("INSERT INTO recipes (slug, data) VALUES (:slug, :data)");
foreach ($data as $recipe) {
if (isset($recipe['slug'])) {
$insert->execute([
'slug' => $recipe['slug'],
'data' => json_encode($recipe, JSON_UNESCAPED_UNICODE)
]);
}
function firestore_get(string $url): ?array {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || !$response) {
return null;
}
return json_decode($response, true);
}
}
}
} catch (PDOException $e) {
error_log("DB Init Error: " . $e->getMessage());
}
}
function load_recipes_local(): array {
$pdo = get_db_connection();
$data = [];
if (!$pdo) {
$path = __DIR__ . '/data/recipes.json';
if (!file_exists($path)) {
return [];
}
if (file_exists($path)) {
$json = file_get_contents($path);
$data = json_decode($json, true);
if (!is_array($data)) {
return [];
$data = json_decode($json, true) ?: [];
}
} else {
static $initialized = false;
if (!$initialized) {
init_db();
$initialized = true;
}
try {
$stmt = $pdo->query("SELECT data FROM recipes");
if ($stmt) {
while ($row = $stmt->fetch()) {
$recipe = json_decode($row['data'], true);
if (is_array($recipe)) {
$data[] = $recipe;
}
}
}
} catch (PDOException $e) {
error_log("Failed to load recipes from DB: " . $e->getMessage());
}
}
foreach ($data as &$recipe) {
@@ -68,59 +86,61 @@ function load_recipes_local(): array {
}
function load_recipes(): array {
$config = get_firebase_config();
$projectId = $config['projectId'] ?? '';
if (empty($projectId)) {
return load_recipes_local();
}
$url = "https://firestore.googleapis.com/v1/projects/{$projectId}/databases/(default)/documents/recipes?pageSize=100";
$res = firestore_get($url);
if (!$res || !isset($res['documents'])) {
return load_recipes_local();
}
$recipes = [];
foreach ($res['documents'] as $doc) {
$decoded = decode_firestore_document($doc);
if (!empty($decoded)) {
if (!empty($decoded['hero']) && is_string($decoded['hero'])) {
$decoded['hero'] = normalize_asset_path($decoded['hero']);
}
$recipes[] = $decoded;
}
}
return $recipes;
}
function load_recipe_by_slug(string $slug): ?array {
$config = get_firebase_config();
$projectId = $config['projectId'] ?? '';
if (empty($projectId)) {
return find_recipe_by_slug(load_recipes_local(), $slug);
}
$url = "https://firestore.googleapis.com/v1/projects/{$projectId}/databases/(default)/documents/recipes/" . urlencode($slug);
$res = firestore_get($url);
if (!$res || isset($res['error'])) {
return find_recipe_by_slug(load_recipes_local(), $slug);
}
$decoded = decode_firestore_document($res);
if (!empty($decoded['hero']) && is_string($decoded['hero'])) {
$decoded['hero'] = normalize_asset_path($decoded['hero']);
}
return $decoded;
}
function save_recipes(array $recipes): bool {
$pdo = get_db_connection();
$path = __DIR__ . '/data/recipes.json';
if (!$pdo) {
$json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return (bool) file_put_contents($path, $json);
}
try {
$pdo->beginTransaction();
$slugs = [];
$insert = $pdo->prepare("INSERT INTO recipes (slug, data) VALUES (:slug, :data) ON CONFLICT (slug) DO UPDATE SET data = EXCLUDED.data, updated_at = CURRENT_TIMESTAMP");
foreach ($recipes as $recipe) {
if (isset($recipe['slug'])) {
$slugs[] = $recipe['slug'];
$insert->execute([
'slug' => $recipe['slug'],
'data' => json_encode($recipe, JSON_UNESCAPED_UNICODE)
]);
}
}
if (!empty($slugs)) {
$placeholders = implode(',', array_fill(0, count($slugs), '?'));
$delete = $pdo->prepare("DELETE FROM recipes WHERE slug NOT IN ($placeholders)");
$delete->execute($slugs);
} else {
$pdo->exec("DELETE FROM recipes");
}
$pdo->commit();
// Also update local JSON as backup
$json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents($path, $json);
return true;
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log("Failed to save recipes to DB: " . $e->getMessage());
return false;
}
}
/**
* Default settings for imprint and privacy pages.
*/