- Implemented the full Fat Bottom Grille storefront and staff dashboard.

Highlights include configurable menus, specials, cart/checkout, taxes, 
customer accounts with email 2FA, newsletters, analytics, refunds, 
PDF/CSV exports, Square/Mailgun adapters, and optional MySQL migration.

See README.md for setup and production configuration. Verified 
PHP/JavaScript syntax, responsive layouts, registration, 2FA, checkout, 
dashboard workflows, settings persistence, and PDF generation.
Real Square, Mailgun, and MySQL connections require credentials/server 
access. Square implementation follows the official Payments API and 
Refunds API.
This commit is contained in:
Ty Clifford
2026-06-10 13:27:42 -04:00
parent d46b0be488
commit 16235369cb
49 changed files with 7685 additions and 2 deletions
+97
View File
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace FatBottom\Services;
use FatBottom\Core\Config;
use PDO;
use RuntimeException;
final class MySqlMigrator
{
public function __construct(private readonly Config $config)
{
}
public function migrate(): array
{
$sqlitePath = BASE_PATH . '/storage/database/store.sqlite';
if (!is_file($sqlitePath)) {
throw new RuntimeException('The SQLite store database does not exist yet.');
}
if (!extension_loaded('pdo_mysql')) {
throw new RuntimeException('The pdo_mysql PHP extension is required for migration.');
}
$mysql = new PDO(
sprintf(
'mysql:host=%s;port=%d;charset=utf8mb4',
$this->config->get('database.mysql_host'),
(int) $this->config->get('database.mysql_port', 3306)
),
(string) $this->config->get('database.mysql_username'),
(string) $this->config->get('database.mysql_password'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$database = preg_replace('/[^a-zA-Z0-9_]/', '', (string) $this->config->get('database.mysql_database'));
if ($database === '') {
throw new RuntimeException('Enter a valid MySQL database name.');
}
$mysql->exec("CREATE DATABASE IF NOT EXISTS `{$database}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$mysql->exec("USE `{$database}`");
$sqlite = new PDO('sqlite:' . $sqlitePath, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$tables = $sqlite->query(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
)->fetchAll(PDO::FETCH_COLUMN);
$rowCount = 0;
$mysql->exec('SET FOREIGN_KEY_CHECKS = 0');
foreach ($tables as $table) {
$columns = $sqlite->query("PRAGMA table_info(`{$table}`)")->fetchAll(PDO::FETCH_ASSOC);
$definitions = [];
$primary = [];
foreach ($columns as $column) {
$type = strtoupper((string) $column['type']);
$mysqlType = str_contains($type, 'INT') ? 'BIGINT'
: (str_contains($type, 'REAL') || str_contains($type, 'FLOA') ? 'DOUBLE' : 'TEXT');
if ((int) $column['pk'] === 1 && $column['name'] === 'id') {
$definitions[] = '`id` BIGINT NOT NULL AUTO_INCREMENT';
$primary[] = '`id`';
} else {
$definitions[] = sprintf(
'`%s` %s %s',
str_replace('`', '', (string) $column['name']),
$mysqlType,
(int) $column['notnull'] === 1 ? 'NOT NULL' : 'NULL'
);
if ((int) $column['pk'] === 1) {
$primary[] = '`' . str_replace('`', '', (string) $column['name']) . '`';
}
}
}
if ($primary !== []) {
$definitions[] = 'PRIMARY KEY (' . implode(', ', $primary) . ')';
}
$mysql->exec("DROP TABLE IF EXISTS `{$table}`");
$mysql->exec("CREATE TABLE `{$table}` (" . implode(', ', $definitions) . ') ENGINE=InnoDB');
$rows = $sqlite->query("SELECT * FROM `{$table}`")->fetchAll(PDO::FETCH_ASSOC);
if ($rows !== []) {
$names = array_keys($rows[0]);
$placeholders = implode(', ', array_fill(0, count($names), '?'));
$quotedNames = implode(', ', array_map(static fn (string $name): string => "`{$name}`", $names));
$insert = $mysql->prepare("INSERT INTO `{$table}` ({$quotedNames}) VALUES ({$placeholders})");
foreach ($rows as $row) {
$insert->execute(array_values($row));
$rowCount++;
}
}
}
$mysql->exec('SET FOREIGN_KEY_CHECKS = 1');
return ['tables' => count($tables), 'rows' => $rowCount];
}
}