You've already forked Atomcms-edit
refactor: integrate diagnostics into Commandocentrum and split EmulatorUpdateService
- Add DiagnosticRunner integration to Commandocentrum for system health display - Refactor EmulatorUpdateService from 2524 lines to 395 lines (facade pattern) - Extract EmulatorStatusService, EmulatorJarService, EmulatorSourceService - Extract EmulatorBuildService, EmulatorSqlService, EmulatorBackupService - Add shared EmulatorConfiguration trait for dependency injection - Preserve backward compatibility on all public methods
This commit is contained in:
Executable
+510
@@ -0,0 +1,510 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Emulator;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class EmulatorSqlService
|
||||
{
|
||||
use EmulatorConfiguration;
|
||||
|
||||
private const string SQL_TABLE = 'emulator_sql_updates';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loadConfiguration();
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! in_array($this->githubUrl, [null, '', '0'], true) || ! in_array($this->jarDirectUrl, [null, '', '0'], true);
|
||||
}
|
||||
|
||||
public function checkForUpdates(bool $recentOnly = true): array
|
||||
{
|
||||
if (! $this->githubRepo) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'error' => 'Geen GitHub repo geconfigureerd',
|
||||
];
|
||||
}
|
||||
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
$result = $this->fetchSqlFilesFromGitHub($recentOnly);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'error' => $result['error'],
|
||||
];
|
||||
}
|
||||
|
||||
$sqlFiles = $result;
|
||||
|
||||
if ($sqlFiles === []) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'message' => $recentOnly
|
||||
? 'Geen SQL updates van de afgelopen week gevonden'
|
||||
: 'Geen SQL updates gevonden',
|
||||
];
|
||||
}
|
||||
|
||||
$appliedHashes = $this->getAppliedSqlHashes();
|
||||
$newSqlFiles = [];
|
||||
$alreadyApplied = [];
|
||||
|
||||
foreach ($sqlFiles as $file) {
|
||||
$hash = $file['sha'] ?? md5((string) $file['name']);
|
||||
if (in_array($hash, $appliedHashes)) {
|
||||
$alreadyApplied[] = $file['name'];
|
||||
|
||||
continue;
|
||||
}
|
||||
$newSqlFiles[] = $file;
|
||||
}
|
||||
|
||||
if ($newSqlFiles === []) {
|
||||
return [
|
||||
'has_updates' => false,
|
||||
'message' => 'Alle SQL updates zijn al toegepast (' . count($alreadyApplied) . ' stuks)',
|
||||
'applied_count' => count($alreadyApplied),
|
||||
];
|
||||
}
|
||||
|
||||
usort($newSqlFiles, fn ($a, $b) => strcmp((string) $a['name'], (string) $b['name']));
|
||||
|
||||
return [
|
||||
'has_updates' => true,
|
||||
'count' => count($newSqlFiles),
|
||||
'files' => $newSqlFiles,
|
||||
'message' => count($newSqlFiles) . ' nieuwe SQL update(s) van deze week',
|
||||
'applied_count' => count($alreadyApplied),
|
||||
];
|
||||
}
|
||||
|
||||
public function runUpdates(): array
|
||||
{
|
||||
$sqlCheck = $this->checkForUpdates(false);
|
||||
|
||||
if (! ($sqlCheck['has_updates'] ?? false)) {
|
||||
return ['success' => true, 'sql_updated' => false, 'message' => 'Geen nieuwe SQL updates'];
|
||||
}
|
||||
|
||||
$results = [
|
||||
'success' => true,
|
||||
'sql_updated' => true,
|
||||
'files_run' => [],
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
foreach ($sqlCheck['files'] as $file) {
|
||||
$sqlResult = $this->downloadAndRunSql($file);
|
||||
|
||||
if ($sqlResult['success']) {
|
||||
$results['files_run'][] = $file['name'];
|
||||
$this->markSqlAsApplied($file);
|
||||
} else {
|
||||
$results['errors'][] = $file['name'] . ': ' . $sqlResult['error'];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($results['errors']) > 0) {
|
||||
$results['message'] = count($results['files_run']) . ' SQL updates succesvol, ' . count($results['errors']) . ' met fouten';
|
||||
} else {
|
||||
$results['message'] = count($results['files_run']) . ' SQL updates succesvol uitgevoerd!';
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getAppliedUpdates(): array
|
||||
{
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
return DB::table(self::SQL_TABLE)
|
||||
->orderBy('applied_at', 'desc')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function diagnose(): array
|
||||
{
|
||||
$diagnosis = [
|
||||
'table_exists' => false,
|
||||
'applied_count' => 0,
|
||||
'pending_count' => 0,
|
||||
'error' => null,
|
||||
];
|
||||
|
||||
try {
|
||||
$diagnosis['table_exists'] = Schema::hasTable(self::SQL_TABLE);
|
||||
|
||||
if ($diagnosis['table_exists']) {
|
||||
$diagnosis['applied_count'] = DB::table(self::SQL_TABLE)->count();
|
||||
}
|
||||
|
||||
if ($this->githubRepo && $diagnosis['table_exists']) {
|
||||
$sqlCheck = $this->checkForUpdates(false);
|
||||
if (isset($sqlCheck['count'])) {
|
||||
$diagnosis['pending_count'] = $sqlCheck['count'];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$diagnosis['error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $diagnosis;
|
||||
}
|
||||
|
||||
public function repair(): array
|
||||
{
|
||||
$actions = [];
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
$this->ensureSqlTableExists();
|
||||
$actions[] = 'SQL update tabel gecontroleerd';
|
||||
|
||||
$appliedCount = DB::table(self::SQL_TABLE)->count();
|
||||
$actions[] = "SQL updates tabel OK ({$appliedCount} records)";
|
||||
|
||||
if ($this->githubRepo) {
|
||||
$sqlCheck = $this->checkForUpdates(false);
|
||||
|
||||
if (isset($sqlCheck['error'])) {
|
||||
$actions[] = 'SQL update check: ' . $sqlCheck['error'];
|
||||
} elseif (! ($sqlCheck['has_updates'] ?? false)) {
|
||||
$actions[] = 'SQL updates: ' . ($sqlCheck['message'] ?? 'Allemaal up-to-date');
|
||||
} else {
|
||||
$count = $sqlCheck['count'] ?? 0;
|
||||
$actions[] = "{$count} nieuwe SQL updates beschikbaar";
|
||||
|
||||
if ($this->isConfigured()) {
|
||||
$actions[] = 'SQL updates worden toegepast...';
|
||||
$runResult = $this->runUpdates();
|
||||
|
||||
if ($runResult['success'] ?? false) {
|
||||
$filesRun = count($runResult['files_run'] ?? []);
|
||||
$actions[] = "{$filesRun} SQL updates toegepast";
|
||||
} else {
|
||||
$errors[] = 'SQL updates: ' . ($runResult['error'] ?? 'Onbekende fout');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = 'SQL repair: ' . $e->getMessage();
|
||||
Log::error('[EmulatorSql] Repair failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $errors === [],
|
||||
'actions' => $actions,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureSqlTableExists(): void
|
||||
{
|
||||
if (Schema::hasTable(self::SQL_TABLE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create(self::SQL_TABLE, function ($table) {
|
||||
$table->id();
|
||||
$table->string('file_name');
|
||||
$table->string('file_hash')->unique();
|
||||
$table->timestamp('applied_at');
|
||||
$table->text('sql_content')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
private function getAppliedSqlHashes(): array
|
||||
{
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
return DB::table(self::SQL_TABLE)
|
||||
->pluck('file_hash')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function markSqlAsApplied(array $file): void
|
||||
{
|
||||
$this->ensureSqlTableExists();
|
||||
|
||||
$hash = $file['sha'] ?? md5((string) $file['name']);
|
||||
|
||||
DB::table(self::SQL_TABLE)->updateOrInsert(
|
||||
['file_hash' => $hash],
|
||||
[
|
||||
'file_name' => $file['name'],
|
||||
'applied_at' => now(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function fetchSqlFilesFromGitHub(bool $recentOnly = false): array
|
||||
{
|
||||
if (! $this->githubRepo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$branch = $this->githubBranch ?: 'main';
|
||||
|
||||
$folderNames = [
|
||||
'Database%20Updates',
|
||||
'Database Updates',
|
||||
'database_updates',
|
||||
'database/updates',
|
||||
'sql/updates',
|
||||
'sql',
|
||||
'updates',
|
||||
];
|
||||
|
||||
$knownSqlFiles = [
|
||||
'07012026_UpdateDatabase_to_4-0-1.sql',
|
||||
'09012026_UpdateDatabase_to_4-0-2.sql',
|
||||
'12012026_Battle Banzai.sql',
|
||||
'12012026_Breeding Fixes.sql',
|
||||
'12012026_ChatBubbles.sql',
|
||||
'16032026_updateall_command.sql',
|
||||
'17032026_allow_underpass.sql',
|
||||
'19032026_hotel_timezone.sql',
|
||||
'21022026_user_prefixes.sql',
|
||||
'Default_Camera.sql',
|
||||
'UpdateDatabase_Allow_diagonale.sql',
|
||||
'UpdateDatabase_BOT.sql',
|
||||
'UpdateDatabase_Banners.sql',
|
||||
'UpdateDatabase_DanceCMD.sql',
|
||||
'UpdateDatabase_Happiness.sql',
|
||||
'UpdateDatabase_Websocket.sql',
|
||||
'UpdateDatabase_unignorable.sql',
|
||||
];
|
||||
|
||||
try {
|
||||
if ($recentOnly) {
|
||||
return $this->fetchRecentSqlFiles($branch);
|
||||
}
|
||||
|
||||
$sqlFiles = [];
|
||||
|
||||
foreach ($knownSqlFiles as $filename) {
|
||||
foreach ($folderNames as $folderName) {
|
||||
$encodedFilename = str_replace(' ', '%20', $filename);
|
||||
$url = "https://raw.githubusercontent.com/{$this->githubRepo}/{$branch}/{$folderName}/{$encodedFilename}";
|
||||
$response = Http::timeout(10)->get($url);
|
||||
|
||||
if ($response->successful()) {
|
||||
$sqlFiles[] = [
|
||||
'name' => $filename,
|
||||
'url' => $url,
|
||||
'sha' => md5($response->body()),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sqlFiles !== []) {
|
||||
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $sqlFiles;
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorSql] Could not fetch SQL files', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchRecentSqlFiles(string $branch): array
|
||||
{
|
||||
$weekAgo = now()->subDays(7)->toIso8601String();
|
||||
|
||||
$githubToken = setting('github_token', '');
|
||||
$headers = [
|
||||
'Accept' => 'application/vnd.github+json',
|
||||
'User-Agent' => 'AtomCMS-EmulatorUpdate/1.0',
|
||||
];
|
||||
if (! empty($githubToken)) {
|
||||
$headers['Authorization'] = 'Bearer ' . $githubToken;
|
||||
}
|
||||
|
||||
$folderNames = ['Database Updates', 'database_updates', 'sql', 'updates'];
|
||||
|
||||
try {
|
||||
foreach ($folderNames as $folderName) {
|
||||
$response = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits", [
|
||||
'path' => $folderName,
|
||||
'sha' => $branch,
|
||||
'since' => $weekAgo,
|
||||
'per_page' => 100,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$commits = $response->json();
|
||||
|
||||
if (! is_array($commits) || $commits === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sqlFiles = [];
|
||||
|
||||
foreach ($commits as $commit) {
|
||||
$sha = $commit['sha'] ?? null;
|
||||
$commitDate = $commit['commit']['committer']['date'] ?? null;
|
||||
|
||||
if (! $sha) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commitResponse = Http::withHeaders($headers)->timeout(30)->get("https://api.github.com/repos/{$this->githubRepo}/commits/{$sha}");
|
||||
|
||||
if (! $commitResponse->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commitData = $commitResponse->json();
|
||||
$files = $commitData['files'] ?? [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filename = $file['filename'] ?? '';
|
||||
|
||||
if (! str_ends_with(strtolower((string) $filename), '.sql')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = basename((string) $filename);
|
||||
$sqlFiles[] = [
|
||||
'name' => $name,
|
||||
'url' => $file['raw_url'] ?? "https://raw.githubusercontent.com/{$this->githubRepo}/{$sha}/{$filename}",
|
||||
'sha' => $sha,
|
||||
'date' => $commitDate,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($sqlFiles !== []) {
|
||||
usort($sqlFiles, fn ($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $sqlFiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[EmulatorSql] Could not fetch recent SQL files', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function downloadAndRunSql(array $file): array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(60)->get($file['url']);
|
||||
|
||||
if (! $response->successful()) {
|
||||
return ['success' => false, 'error' => 'Download mislukt'];
|
||||
}
|
||||
|
||||
$sql = $this->cleanSql($response->body());
|
||||
|
||||
if (in_array(trim($sql), ['', '0'], true)) {
|
||||
return ['success' => true, 'message' => 'Lege SQL file overgeslagen'];
|
||||
}
|
||||
|
||||
$host = setting('emulator_database_host', config('database.connections.emulator.host', '127.0.0.1'));
|
||||
$port = setting('emulator_database_port', config('database.connections.emulator.port', '3306'));
|
||||
$name = setting('emulator_database_name', config('database.connections.emulator.database', ''));
|
||||
$username = setting('emulator_database_username', config('database.connections.emulator.username', ''));
|
||||
$password = setting('emulator_database_password', config('database.connections.emulator.password', ''));
|
||||
|
||||
if (empty($name) || empty($username)) {
|
||||
return ['success' => false, 'error' => 'Emulator database niet geconfigureerd'];
|
||||
}
|
||||
|
||||
$pdo = new \PDO(
|
||||
"mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
|
||||
$username,
|
||||
$password,
|
||||
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION],
|
||||
);
|
||||
|
||||
$statements = $this->splitSqlStatements($sql);
|
||||
$successCount = 0;
|
||||
$skipCount = 0;
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim((string) $statement);
|
||||
if ($statement === '' || $statement === '0') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->exec($statement);
|
||||
$successCount++;
|
||||
} catch (\PDOException $e) {
|
||||
if ($this->isDuplicateError($e)) {
|
||||
$skipCount++;
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::warning('[SQL Update] Statement error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('[SQL Update] Successfully ran: ' . $file['name'] . " ({$successCount} statements, {$skipCount} skipped)");
|
||||
|
||||
return ['success' => true, 'message' => "SQL uitgevoerd ({$successCount} statements, {$skipCount} duplicate)"];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[SQL Update] Failed: ' . $file['name'], ['error' => $e->getMessage()]);
|
||||
|
||||
return ['success' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanSql(string $sql): string
|
||||
{
|
||||
$sql = preg_replace('/--.*$/m', '', $sql);
|
||||
$sql = preg_replace('/#.*$/m', '', (string) $sql);
|
||||
$sql = preg_replace('/SET FOREIGN_KEY_CHECKS.*?;/i', '', (string) $sql);
|
||||
|
||||
return preg_replace('/SET @@SESSION.SQL_MODE.*?;/i', '', (string) $sql);
|
||||
}
|
||||
|
||||
private function splitSqlStatements(string $sql): array
|
||||
{
|
||||
$parts = explode(';', $sql);
|
||||
|
||||
return array_filter($parts, fn ($part) => trim((string) $part) !== '');
|
||||
}
|
||||
|
||||
private function isDuplicateError(\PDOException $e): bool
|
||||
{
|
||||
$message = strtolower($e->getMessage());
|
||||
$patterns = [
|
||||
'duplicate entry',
|
||||
"table '",
|
||||
'already exists',
|
||||
"can't create",
|
||||
'duplicate key',
|
||||
'duplicate index',
|
||||
'error 1061',
|
||||
'error 1062',
|
||||
'error 1826',
|
||||
];
|
||||
|
||||
return array_any($patterns, fn ($pattern) => str_contains($message, (string) $pattern));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user