Files
MokoJoomBackup/source/packages/com_mokobackup/src/Engine/DatabaseDumper.php
T
Jonathan Miller a13f7ca6a6
Generic: Repo Health / Release configuration (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: Auto Version Bump / Version Bump (push) Failing after 3s
chore: rename src/ to source/ per MokoStandards convention
Update all references in Makefile, manifest.xml, .gitignore, and CI
workflows (ci-joomla, pr-check, repo-health) to use source/ as the
primary directory with src/ as a fallback for compatibility.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 08:08:33 -05:00

222 lines
5.3 KiB
PHP

<?php
/**
* @package MokoJoomBackup
* @subpackage com_mokobackup
* @author Moko Consulting <hello@mokoconsulting.tech>
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*/
namespace Joomla\Component\MokoBackup\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[] = '-- MokoJoomBackup Database Dump';
$output[] = '-- Generated: ' . date('Y-m-d H:i:s');
$output[] = '-- Server: ' . $db->getServerType();
$output[] = '-- Database: ' . $db->getName();
$output[] = '-- Prefix: ' . $prefix;
$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: ' . $table;
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;
}
$output[] = 'DROP TABLE IF EXISTS ' . $db->quoteName($table) . ';';
$output[] = $createRow[1] . ';';
$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 ' . $db->quoteName($table)
. ' (' . 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;
}
}