Update: 2026-05-06 21:24:56
This commit is contained in:
@@ -19,6 +19,10 @@ class Cache
|
|||||||
$pass = env('REDIS_PASSWORD', null);
|
$pass = env('REDIS_PASSWORD', null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!class_exists('\Predis\Client')) {
|
||||||
|
throw new \Exception('Predis client is not installed. Please run composer install.');
|
||||||
|
}
|
||||||
|
|
||||||
self::$client = new \Predis\Client([
|
self::$client = new \Predis\Client([
|
||||||
'scheme' => 'tcp',
|
'scheme' => 'tcp',
|
||||||
'host' => $host,
|
'host' => $host,
|
||||||
@@ -26,7 +30,7 @@ class Cache
|
|||||||
'password' => $pass,
|
'password' => $pass,
|
||||||
]);
|
]);
|
||||||
self::$client->connect();
|
self::$client->connect();
|
||||||
} catch (\Exception $e) {
|
} catch (\Throwable $e) { // Catch \Throwable instead of \Exception to catch fatal class errors
|
||||||
error_log("Redis Connection Error: " . $e->getMessage());
|
error_log("Redis Connection Error: " . $e->getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ define('STORAGE_PATH', ROOT_PATH . '/storage');
|
|||||||
require_once APP_PATH . '/bootstrap/env.php';
|
require_once APP_PATH . '/bootstrap/env.php';
|
||||||
require_once APP_PATH . '/helpers/helpers.php';
|
require_once APP_PATH . '/helpers/helpers.php';
|
||||||
|
|
||||||
|
// Load Composer Autoloader
|
||||||
|
$vendorAutoload = ROOT_PATH . '/vendor/autoload.php';
|
||||||
|
if (file_exists($vendorAutoload)) {
|
||||||
|
require_once $vendorAutoload;
|
||||||
|
}
|
||||||
|
|
||||||
// Self-healing Storage
|
// Self-healing Storage
|
||||||
$dirs = ['/cache', '/logs', '/invoices', '/exports'];
|
$dirs = ['/cache', '/logs', '/invoices', '/exports'];
|
||||||
foreach ($dirs as $d) {
|
foreach ($dirs as $d) {
|
||||||
|
|||||||
@@ -41,10 +41,14 @@ try {
|
|||||||
$stmt->execute([$phoneHash]);
|
$stmt->execute([$phoneHash]);
|
||||||
$user = $stmt->fetch();
|
$user = $stmt->fetch();
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
// Fallback to searching by plain phone if phone_hash column doesn't exist
|
try {
|
||||||
$stmt = $db->prepare("SELECT id, tenant_id, name, is_active FROM users WHERE phone = ? LIMIT 1");
|
// Fallback to searching by plain phone if phone_hash column doesn't exist
|
||||||
$stmt->execute([$phone]);
|
$stmt = $db->prepare("SELECT id, tenant_id, name, is_active FROM users WHERE phone = ? LIMIT 1");
|
||||||
$user = $stmt->fetch();
|
$stmt->execute([$phone]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
} catch (\PDOException $fallbackException) {
|
||||||
|
json_error('حدث خطأ في قاعدة البيانات: ' . $fallbackException->getMessage(), 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ if (!in_array($data['role'] ?? '', $allowedRoles, true)) {
|
|||||||
$errors = Validator::validate($data, [
|
$errors = Validator::validate($data, [
|
||||||
'name' => 'required',
|
'name' => 'required',
|
||||||
'email' => 'required|email',
|
'email' => 'required|email',
|
||||||
|
'phone' => 'required',
|
||||||
'password' => 'required',
|
'password' => 'required',
|
||||||
'role' => 'required'
|
'role' => 'required'
|
||||||
]);
|
]);
|
||||||
@@ -45,6 +46,9 @@ $encryptedName = Encryption::encrypt($data['name']);
|
|||||||
$encryptedEmail = Encryption::encrypt($data['email']);
|
$encryptedEmail = Encryption::encrypt($data['email']);
|
||||||
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
|
$emailHash = hash('sha256', strtolower($data['email'])); // For fast lookup during login
|
||||||
|
|
||||||
|
$encryptedPhone = Encryption::encrypt($data['phone']);
|
||||||
|
$phoneHash = hash('sha256', preg_replace('/[^0-9+]/', '', $data['phone']));
|
||||||
|
|
||||||
// 3. Determine Tenant ID
|
// 3. Determine Tenant ID
|
||||||
$tenantId = null;
|
$tenantId = null;
|
||||||
if ($decoded['role'] === 'super_admin') {
|
if ($decoded['role'] === 'super_admin') {
|
||||||
@@ -62,13 +66,15 @@ if ($decoded['role'] === 'super_admin') {
|
|||||||
|
|
||||||
// 4. Save to Database
|
// 4. Save to Database
|
||||||
try {
|
try {
|
||||||
$stmt = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
$stmt = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, phone, phone_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
\App\Core\Database::generateUuid(),
|
\App\Core\Database::generateUuid(),
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$encryptedName,
|
$encryptedName,
|
||||||
$encryptedEmail,
|
$encryptedEmail,
|
||||||
$emailHash,
|
$emailHash,
|
||||||
|
$encryptedPhone,
|
||||||
|
$phoneHash,
|
||||||
password_hash($data['password'], PASSWORD_DEFAULT),
|
password_hash($data['password'], PASSWORD_DEFAULT),
|
||||||
$data['role'],
|
$data['role'],
|
||||||
date('Y-m-d H:i:s')
|
date('Y-m-d H:i:s')
|
||||||
|
|||||||
34
public/migrate_db.php
Normal file
34
public/migrate_db.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/app/bootstrap/init.php';
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "Checking columns in 'users' table...\n";
|
||||||
|
$stmt = $db->query("DESCRIBE users");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
if (!in_array('phone_hash', $columns)) {
|
||||||
|
echo "Adding 'phone_hash' column...\n";
|
||||||
|
$db->exec("ALTER TABLE users ADD COLUMN phone_hash VARCHAR(64) NULL AFTER phone");
|
||||||
|
$db->exec("CREATE INDEX idx_phone_hash ON users(phone_hash)");
|
||||||
|
echo "Column 'phone_hash' added successfully.\n";
|
||||||
|
} else {
|
||||||
|
echo "Column 'phone_hash' already exists.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array('email_hash', $columns)) {
|
||||||
|
echo "Adding 'email_hash' column...\n";
|
||||||
|
$db->exec("ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) NULL AFTER email");
|
||||||
|
$db->exec("CREATE INDEX idx_email_hash ON users(email_hash)");
|
||||||
|
echo "Column 'email_hash' added successfully.\n";
|
||||||
|
} else {
|
||||||
|
echo "Column 'email_hash' already exists.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Migration completed.\n";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
@@ -53,6 +53,84 @@ try {
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## File: `update_phone.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Update User Phone Script (Secure)
|
||||||
|
* Run: php scripts/update_phone.php --email=admin@musadaq.com --phone=963992952235
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
use App\Core\Encryption;
|
||||||
|
|
||||||
|
// Parse CLI arguments
|
||||||
|
$options = getopt("", ["email:", "phone:", "id:"]);
|
||||||
|
$email = $options['email'] ?? null;
|
||||||
|
$phone = $options['phone'] ?? null;
|
||||||
|
$id = $options['id'] ?? null;
|
||||||
|
|
||||||
|
if ((!$email && !$id) || !$phone) {
|
||||||
|
die("Usage: php scripts/update_phone.php --phone=yourphone [--email=your@email.com | --id=user-uuid]\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// 1. Sanitize phone
|
||||||
|
try {
|
||||||
|
$cleanPhone = preg_replace('/[^0-9+]/', '', $phone);
|
||||||
|
$phoneHash = hash('sha256', $cleanPhone);
|
||||||
|
$encryptedPhone = Encryption::encrypt($cleanPhone);
|
||||||
|
|
||||||
|
// 2. Update user
|
||||||
|
if ($id) {
|
||||||
|
$stmt = $db->prepare("UPDATE users SET phone = ?, phone_hash = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$encryptedPhone, $phoneHash, $id]);
|
||||||
|
$identifier = "ID $id";
|
||||||
|
} else {
|
||||||
|
// Note: Searching by encrypted email will likely fail due to IV randomness. Use ID.
|
||||||
|
$stmt = $db->prepare("UPDATE users SET phone = ?, phone_hash = ? WHERE email = ?");
|
||||||
|
$stmt->execute([$encryptedPhone, $phoneHash, $email]);
|
||||||
|
$identifier = "email $email";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
echo "✅ Success! Phone updated for $identifier\n";
|
||||||
|
echo " Encrypted: $encryptedPhone\n";
|
||||||
|
echo " Hash: $phoneHash\n";
|
||||||
|
} else {
|
||||||
|
echo "❌ Failed. User with $identifier not found or no changes made.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## File: `list_users.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
$users = $db->query("SELECT id, email, name FROM users")->fetchAll();
|
||||||
|
|
||||||
|
echo "ID | Email | Name\n";
|
||||||
|
echo "----------------------\n";
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$email = App\Core\Encryption::decrypt($user['email']) ?: $user['email'];
|
||||||
|
$name = App\Core\Encryption::decrypt($user['name']) ?: $user['name'];
|
||||||
|
echo "{$user['id']} | $email | $name\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## File: `schema.sql`
|
## File: `schema.sql`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
@@ -79,6 +157,7 @@ CREATE TABLE users (
|
|||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
email VARCHAR(255) NOT NULL,
|
email VARCHAR(255) NOT NULL,
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
phone VARCHAR(255),
|
||||||
role ENUM('super_admin','admin','accountant','viewer') NOT NULL,
|
role ENUM('super_admin','admin','accountant','viewer') NOT NULL,
|
||||||
company_id CHAR(36) NULL, -- assigned company for accountant
|
company_id CHAR(36) NULL, -- assigned company for accountant
|
||||||
refresh_token_hash VARCHAR(255) NULL,
|
refresh_token_hash VARCHAR(255) NULL,
|
||||||
@@ -486,6 +565,27 @@ echo "════════════════════════
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## File: `debug_collation.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
$tables = ['users', 'tenants', 'companies'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
echo "Table: $table\n";
|
||||||
|
$stmt = $db->query("SHOW FULL COLUMNS FROM $table WHERE Field = 'id'");
|
||||||
|
$col = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
print_r($col);
|
||||||
|
echo "--------------------------\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## File: `migrate.php`
|
## File: `migrate.php`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
@@ -738,6 +838,175 @@ echo "════════════════════════
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## File: `migrate_phase3_mobile.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Phase 3 Migration: Mobile App Support
|
||||||
|
* Run: php scripts/migrate_phase3_mobile.php
|
||||||
|
*
|
||||||
|
* Adds tables and columns required for:
|
||||||
|
* - Mobile OTP Authentication
|
||||||
|
* - Device Management
|
||||||
|
* - Batch Invoice Upload from Scanner
|
||||||
|
* - Processing Queue for AI extraction
|
||||||
|
* - Notifications System
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||||
|
|
||||||
|
use App\Core\Database;
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
echo "═══════════════════════════════════════════\n";
|
||||||
|
echo " مُصادَق — Phase 3 Migration\n";
|
||||||
|
echo " Mobile App + Batch Processing Support\n";
|
||||||
|
echo "═══════════════════════════════════════════\n\n";
|
||||||
|
|
||||||
|
// --- Fetch Parent Collation dynamically ---
|
||||||
|
$stmt = $db->query("SHOW FULL COLUMNS FROM users WHERE Field = 'id'");
|
||||||
|
$userCol = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$charsetCollation = "";
|
||||||
|
if ($userCol && !empty($userCol['Collation'])) {
|
||||||
|
$collation = $userCol['Collation'];
|
||||||
|
list($charset) = explode('_', $collation);
|
||||||
|
$charsetCollation = "CHARACTER SET {$charset} COLLATE {$collation}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrations = [
|
||||||
|
|
||||||
|
// ─── 1. User Device Management ─────────────────────────
|
||||||
|
'create_user_devices' => "
|
||||||
|
CREATE TABLE IF NOT EXISTS user_devices (
|
||||||
|
id CHAR(36) {$charsetCollation} PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
user_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
device_fingerprint VARCHAR(64) NOT NULL,
|
||||||
|
device_name VARCHAR(100) NULL,
|
||||||
|
platform ENUM('android','ios','web') NOT NULL DEFAULT 'android',
|
||||||
|
app_version VARCHAR(20) NULL,
|
||||||
|
push_token TEXT NULL,
|
||||||
|
device_secret VARCHAR(128) NULL,
|
||||||
|
is_trusted BOOLEAN DEFAULT FALSE,
|
||||||
|
last_seen_at DATETIME NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY uq_user_device (user_id, device_fingerprint),
|
||||||
|
INDEX idx_device_fingerprint (device_fingerprint)
|
||||||
|
)
|
||||||
|
",
|
||||||
|
|
||||||
|
// ─── 2. Users table: Add phone + mobile fields ─────────
|
||||||
|
'add_users_phone' => "ALTER TABLE users ADD COLUMN phone VARCHAR(255) NULL AFTER email",
|
||||||
|
'add_users_phone_hash' => "ALTER TABLE users ADD COLUMN phone_hash VARCHAR(64) NULL AFTER phone",
|
||||||
|
'add_users_pin_hash' => "ALTER TABLE users ADD COLUMN pin_hash VARCHAR(255) NULL AFTER password_hash",
|
||||||
|
'add_users_biometric' => "ALTER TABLE users ADD COLUMN biometric_enabled BOOLEAN DEFAULT FALSE AFTER pin_hash",
|
||||||
|
'add_users_phone_index' => "CREATE INDEX idx_phone_hash ON users(phone_hash)",
|
||||||
|
|
||||||
|
// ─── 3. Invoice Batches (Mobile Scanner) ───────────────
|
||||||
|
'create_invoice_batches' => "
|
||||||
|
CREATE TABLE IF NOT EXISTS invoice_batches (
|
||||||
|
id CHAR(36) {$charsetCollation} PRIMARY KEY,
|
||||||
|
tenant_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
company_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
uploaded_by CHAR(36) {$charsetCollation} NULL,
|
||||||
|
total_images INT NOT NULL DEFAULT 0,
|
||||||
|
processed_images INT NOT NULL DEFAULT 0,
|
||||||
|
failed_images INT NOT NULL DEFAULT 0,
|
||||||
|
status ENUM('uploading','processing','done','partial_fail','failed') DEFAULT 'uploading',
|
||||||
|
source ENUM('mobile_scan','web_upload','whatsapp') DEFAULT 'mobile_scan',
|
||||||
|
pdf_path VARCHAR(500) NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
completed_at DATETIME NULL,
|
||||||
|
INDEX idx_tenant_status (tenant_id, status),
|
||||||
|
INDEX idx_company (company_id),
|
||||||
|
INDEX idx_uploaded_by (uploaded_by),
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
)
|
||||||
|
",
|
||||||
|
|
||||||
|
// ─── 4. Invoice Processing Queue ───────────────────────
|
||||||
|
'create_processing_queue' => "
|
||||||
|
CREATE TABLE IF NOT EXISTS invoice_processing_queue (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
batch_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
invoice_id CHAR(36) {$charsetCollation} NULL,
|
||||||
|
tenant_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
company_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
image_path VARCHAR(500) NOT NULL,
|
||||||
|
image_order INT NOT NULL DEFAULT 0,
|
||||||
|
status ENUM('pending','processing','done','failed') DEFAULT 'pending',
|
||||||
|
attempts INT DEFAULT 0,
|
||||||
|
max_attempts INT DEFAULT 3,
|
||||||
|
error_message TEXT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
processed_at DATETIME NULL,
|
||||||
|
INDEX idx_status_tenant (status, tenant_id),
|
||||||
|
INDEX idx_batch (batch_id),
|
||||||
|
INDEX idx_pending (status, attempts)
|
||||||
|
)
|
||||||
|
",
|
||||||
|
|
||||||
|
// ─── 5. Add batch_id to invoices table ─────────────────
|
||||||
|
'add_invoices_batch_id' => "ALTER TABLE invoices ADD COLUMN batch_id CHAR(36) NULL AFTER company_id",
|
||||||
|
'add_invoices_batch_index' => "CREATE INDEX idx_batch_id ON invoices(batch_id)",
|
||||||
|
|
||||||
|
// ─── 6. Notifications Table ────────────────────────────
|
||||||
|
'create_notifications' => "
|
||||||
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
id CHAR(36) {$charsetCollation} PRIMARY KEY DEFAULT (UUID()),
|
||||||
|
tenant_id CHAR(36) {$charsetCollation} NOT NULL,
|
||||||
|
user_id CHAR(36) {$charsetCollation} NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
body TEXT NULL,
|
||||||
|
data JSON NULL,
|
||||||
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
|
read_at DATETIME NULL,
|
||||||
|
push_sent BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_user_unread (user_id, is_read),
|
||||||
|
INDEX idx_tenant (tenant_id),
|
||||||
|
INDEX idx_type (type),
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
",
|
||||||
|
];
|
||||||
|
|
||||||
|
$success = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($migrations as $name => $sql) {
|
||||||
|
try {
|
||||||
|
$db->exec($sql);
|
||||||
|
echo " ✅ {$name}\n";
|
||||||
|
$success++;
|
||||||
|
} catch (\PDOException $e) {
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
if (str_contains($msg, 'Duplicate column') || str_contains($msg, 'Duplicate key name') || str_contains($msg, 'already exists')) {
|
||||||
|
echo " ⏭️ {$name} (already exists)\n";
|
||||||
|
$skipped++;
|
||||||
|
} else {
|
||||||
|
echo " ❌ {$name}: {$msg}\n";
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n═══════════════════════════════════════════\n";
|
||||||
|
echo " Migration Complete!\n";
|
||||||
|
echo " ✅ Success: {$success} | ⏭️ Skipped: {$skipped} | ❌ Failed: {$failed}\n";
|
||||||
|
echo "═══════════════════════════════════════════\n";
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## File: `debug_data.php`
|
## File: `debug_data.php`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
|
|||||||
Reference in New Issue
Block a user