Dev #9

Merged
LordSchmackes merged 13 commits from dev into main 2026-05-23 21:30:54 +00:00
2 changed files with 146 additions and 97 deletions
Showing only changes of commit e7f35d71e3 - Show all commits
+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']);
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)) {
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

This is a concise and efficient way to check for the existence of a column in information_schema.columns.

This is a concise and efficient way to check for the existence of a column in `information_schema.columns`.
$json = file_get_contents($path);
$data = json_decode($json, true);
if (is_array($data) && count($data) > 0) {
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

Loading the schema from an external SQL file (scripts/schema.sql) is a good practice for maintainability and readability. Throwing a RuntimeException if the file is missing is appropriate.

Loading the schema from an external SQL file (`scripts/schema.sql`) is a good practice for maintainability and readability. Throwing a `RuntimeException` if the file is missing is appropriate.
$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)
]);
}
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);
}
}
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

This helper function provides a clean default structure for internationalized recipe data, ensuring consistency.

This helper function provides a clean default structure for internationalized recipe data, ensuring consistency.
} catch (PDOException $e) {
error_log("DB Init Error: " . $e->getMessage());
}
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

The default value for 'description' is an empty string. While functional, for internationalization, it's often beneficial to provide a placeholder string like null or a specific marker (e.g., __('default_description')) that can be explicitly translated or identified as missing. This can help in debugging or ensuring all fields are eventually populated.

The default value for 'description' is an empty string. While functional, for internationalization, it's often beneficial to provide a placeholder string like `null` or a specific marker (e.g., `__('default_description')`) that can be explicitly translated or identified as missing. This can help in debugging or ensuring all fields are eventually populated.
}
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

Similar to the 'description', an empty string for 'category' might be better represented by null or a translatable placeholder to distinguish between an intentionally empty category and a missing one.

Similar to the 'description', an empty string for 'category' might be better represented by `null` or a translatable placeholder to distinguish between an intentionally empty category and a missing one.
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

Similar to 'description' and 'category', an empty string for 'difficulty' could be improved by using null or a translatable placeholder. This helps differentiate between an unset difficulty and a deliberately empty one.

Similar to 'description' and 'category', an empty string for 'difficulty' could be improved by using `null` or a translatable placeholder. This helps differentiate between an unset difficulty and a deliberately empty one.
function load_recipes_local(): array {
$pdo = get_db_connection();
$data = [];
if (!$pdo) {
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

It's good that step_videos is initialized as an empty array. However, the save_recipe function inserts video_url from the input directly. If the input step_videos array contains null or empty strings, they will be inserted as such. Consider adding a trim or filter for empty strings here if they are not intended to be stored.

It's good that `step_videos` is initialized as an empty array. However, the `save_recipe` function inserts `video_url` from the input directly. If the input `step_videos` array contains `null` or empty strings, they will be inserted as such. Consider adding a trim or filter for empty strings here if they are not intended to be stored.
$path = __DIR__ . '/data/recipes.json';
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

Similar to step_videos, step_timers is initialized as an empty array. The save_recipe function converts timer raw values to int or null. If the input array contains non-numeric strings, they will result in null. Ensure that any non-numeric or empty string values are handled consistently, perhaps by filtering them out before insertion.

Similar to `step_videos`, `step_timers` is initialized as an empty array. The `save_recipe` function converts timer raw values to `int` or `null`. If the input array contains non-numeric strings, they will result in `null`. Ensure that any non-numeric or empty string values are handled consistently, perhaps by filtering them out before insertion.
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) ?: [];
}
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

This function correctly extracts base recipe data from a database row, handling potential missing keys with default values. The explicit type casting ((int), (bool)) is good for data integrity.

This function correctly extracts base recipe data from a database row, handling potential missing keys with default values. The explicit type casting (`(int)`, `(bool)`) is good for data integrity.
} 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) {
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

This function attempts to decode legacy JSONB values. It handles arrays, strings, and objects that can be JSON encoded. However, it might be beneficial to add explicit handling or error logging for unexpected data types passed to $value to prevent potential TypeError or other runtime errors if the input is not as expected.

This function attempts to decode legacy JSONB values. It handles arrays, strings, and objects that can be JSON encoded. However, it might be beneficial to add explicit handling or error logging for unexpected data types passed to `$value` to prevent potential `TypeError` or other runtime errors if the input is not as expected.
@@ -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;
}
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;
}
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

This check correctly identifies if the legacy 'data' column exists, preventing unnecessary migration steps. This is a robust way to handle schema evolution.

This check correctly identifies if the legacy 'data' column exists, preventing unnecessary migration steps. This is a robust way to handle schema evolution.
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 = [];
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

Dropping existing tables before applying the schema is a common migration strategy. However, if this script is ever run on a production database with existing data, this will result in data loss. Ensure this is only intended for development/testing environments or that a proper migration system is in place for production.

Dropping existing tables before applying the schema is a common migration strategy. However, if this script is ever run on a production database with existing data, this will result in data loss. Ensure this is only intended for development/testing environments or that a proper migration system is in place for production.
$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), '?'));
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

Using a static variable $ready to ensure schema application only runs once per request is an efficient optimization. This prevents redundant database operations.

Using a static variable `$ready` to ensure schema application only runs once per request is an efficient optimization. This prevents redundant database operations.
$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();
github-actions[bot] commented 2026-05-23 21:30:28 +00:00 (Migrated from github.com)
Review

This function is crucial for database connectivity and schema management. The fallback logic for DATABASE_URL being unset is good. Throwing a DatabaseUnavailableException is a clear way to signal a critical error.

This function is crucial for database connectivity and schema management. The fallback logic for `DATABASE_URL` being unset is good. Throwing a `DatabaseUnavailableException` is a clear way to signal a critical error.
}
error_log("Failed to save recipes to DB: " . $e->getMessage());
return false;
}
}
/**
5