diff --git a/config.php b/config.php index 869259c..441d1a5 100644 --- a/config.php +++ b/config.php @@ -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; + } } diff --git a/helpers.php b/helpers.php index 5379f03..21f8045 100644 --- a/helpers.php +++ b/helpers.php @@ -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']); +function init_db() { + $pdo = get_db_connection(); + if (!$pdo) return; + + 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) + ]); + } + } + } + } + } + } catch (PDOException $e) { + error_log("DB Init Error: " . $e->getMessage()); } - if (isset($value['mapValue']['fields'])) { - return array_map('decode_firestore_value', $value['mapValue']['fields']); - } - return $value; -} - -function decode_firestore_document(array $doc): array { - if (!isset($doc['fields'])) return []; - return array_map('decode_firestore_value', $doc['fields']); -} - -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); } function load_recipes_local(): array { - $path = __DIR__ . '/data/recipes.json'; - if (!file_exists($path)) { - return []; - } - $json = file_get_contents($path); - $data = json_decode($json, true); - if (!is_array($data)) { - return []; + $pdo = get_db_connection(); + $data = []; + + if (!$pdo) { + $path = __DIR__ . '/data/recipes.json'; + if (file_exists($path)) { + $json = file_get_contents($path); + $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,57 +86,59 @@ 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; + return load_recipes_local(); } 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; + return find_recipe_by_slug(load_recipes_local(), $slug); } function save_recipes(array $recipes): bool { + $pdo = get_db_connection(); $path = __DIR__ . '/data/recipes.json'; - $json = json_encode($recipes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - return (bool) file_put_contents($path, $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; + } } /**