* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ namespace Joomla\Component\MokoSuiteBackup\Administrator\Engine; defined('_JEXEC') or die; use Joomla\CMS\Factory; class DatabaseDumper { /** @var array Tables to exclude entirely (both structure and data) */ private array $excludeBoth = []; /** @var array Tables to exclude data only (structure is kept) */ private array $excludeDataOnly = []; /** @var array Tables to exclude structure only (data is kept — unusual) */ private array $excludeStructureOnly = []; private int $tablesCount = 0; /** * @param array $excludeTables Table names to exclude (with #__ prefix). * Supports suffixes: :data-only, :structure-only. * No suffix = exclude both (backward compatible). */ public function __construct(array $excludeTables = []) { foreach ($excludeTables as $entry) { if (str_ends_with($entry, ':data-only')) { $this->excludeDataOnly[] = substr($entry, 0, -10); } elseif (str_ends_with($entry, ':structure-only')) { $this->excludeStructureOnly[] = substr($entry, 0, -15); } else { $this->excludeBoth[] = $entry; } } } /** * Dump all database tables to SQL. * * @return string The SQL dump */ public function dump(): string { $db = Factory::getDbo(); $prefix = $db->getPrefix(); $output = []; $output[] = '-- MokoSuiteBackup Database Dump'; $output[] = '-- Generated: ' . date('Y-m-d H:i:s'); $output[] = '-- Server: ' . $db->getServerType(); $output[] = '-- Database: ' . $db->getName(); $output[] = '-- Original Prefix: ' . $prefix; $output[] = '-- Abstract Prefix: #__'; $output[] = '-- Note: Table names use #__ placeholder. Replace with your prefix on restore.'; $output[] = ''; $output[] = 'SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";'; $output[] = 'SET time_zone = "+00:00";'; $output[] = ''; // Get all tables with the site prefix $tables = $db->getTableList(); $siteTables = []; foreach ($tables as $table) { if (str_starts_with($table, $prefix)) { $siteTables[] = $table; } } foreach ($siteTables as $table) { // Check if excluded $abstractName = '#__' . substr($table, strlen($prefix)); if ($this->isExcludedBoth($abstractName, $table)) { continue; } $skipData = $this->isExcludedDataOnly($abstractName, $table); $skipStructure = $this->isExcludedStructureOnly($abstractName, $table); $this->tablesCount++; $output[] = '-- --------------------------------------------------------'; $output[] = '-- Table: ' . $abstractName; if ($skipData) { $output[] = '-- (data excluded)'; } if ($skipStructure) { $output[] = '-- (structure excluded)'; } $output[] = '-- --------------------------------------------------------'; $output[] = ''; // Get CREATE TABLE statement (unless structure is excluded) if (!$skipStructure) { $db->setQuery('SHOW CREATE TABLE ' . $db->quoteName($table)); $createRow = $db->loadRow(); if (!$createRow || empty($createRow[1])) { continue; } // Replace all occurrences of the live prefix with #__ in CREATE TABLE // output — covers the table itself and FK REFERENCES to other tables $createSql = str_replace('`' . $prefix, '`#__', $createRow[1]); $output[] = 'DROP TABLE IF EXISTS `' . $abstractName . '`;'; $output[] = $createSql . ';'; $output[] = ''; } // Dump data (unless data is excluded) if ($skipData) { $output[] = ''; continue; } $db->setQuery('SELECT COUNT(*) FROM ' . $db->quoteName($table)); $rowCount = (int) $db->loadResult(); if ($rowCount === 0) { $output[] = '-- (empty table)'; $output[] = ''; continue; } $chunkSize = 500; for ($offset = 0; $offset < $rowCount; $offset += $chunkSize) { $db->setQuery( $db->getQuery(true) ->select('*') ->from($db->quoteName($table)), $offset, $chunkSize ); $rows = $db->loadAssocList(); if (empty($rows)) { break; } foreach ($rows as $row) { $values = []; foreach ($row as $value) { if ($value === null) { $values[] = 'NULL'; } else { $values[] = $db->quote($value); } } $columns = array_map([$db, 'quoteName'], array_keys($row)); $output[] = 'INSERT INTO `' . $abstractName . '`' . ' (' . implode(', ', $columns) . ')' . ' VALUES (' . implode(', ', $values) . ');'; } } $output[] = ''; } return implode("\n", $output); } /** * Check if a table is fully excluded (both data and structure). */ private function isExcludedBoth(string $abstractName, string $realName): bool { foreach ($this->excludeBoth as $pattern) { if ($pattern === $abstractName || $pattern === $realName) { return true; } } return false; } /** * Check if a table's data is excluded (structure only). */ private function isExcludedDataOnly(string $abstractName, string $realName): bool { foreach ($this->excludeDataOnly as $pattern) { if ($pattern === $abstractName || $pattern === $realName) { return true; } } return false; } /** * Check if a table's structure is excluded (data only). */ private function isExcludedStructureOnly(string $abstractName, string $realName): bool { foreach ($this->excludeStructureOnly as $pattern) { if ($pattern === $abstractName || $pattern === $realName) { return true; } } return false; } public function getTablesCount(): int { return $this->tablesCount; } }