Release of v5.1.1-beta5

Completely refactors the SQL tweaks and SQL dump classes.
This commit is contained in:
2025-06-25 20:14:59 +00:00
parent af4b12a82b
commit cf681b2b16
20 changed files with 1415 additions and 1921 deletions

View File

@ -15,6 +15,7 @@ namespace VDM\Joomla\Abstraction;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface as JoomlaDatabase;
use VDM\Joomla\Utilities\Component\Helper;
use VDM\Joomla\Database\QuoteTrait;
/**
@ -24,6 +25,13 @@ use VDM\Joomla\Utilities\Component\Helper;
*/
abstract class Database
{
/**
* Function to quote values
*
* @since 5.1.1
*/
use QuoteTrait;
/**
* Database object to query local DB
*
@ -32,14 +40,6 @@ abstract class Database
*/
protected JoomlaDatabase $db;
/**
* Date format to return
*
* @var string
* @since 5.0.2
*/
protected string $dateFormat = 'Y-m-d H:i:s';
/**
* Current component code name
*
@ -70,82 +70,6 @@ abstract class Database
$this->table = '#__' . $this->componentCode;
}
/**
* Safely quote a value for database use, preserving data integrity.
*
* - Native ints/floats passed as-is
* - Clean integer strings are cast to int
* - Clean float strings are cast to float
* - Scientific notation is quoted to preserve original form
* - Leading-zero integers are quoted
* - Dates are formatted and quoted
* - Booleans are converted to TRUE/FALSE
* - Null is converted to NULL
* - All else is quoted with Joomla's db quote
*
* @param mixed $value The value to quote.
*
* @return mixed
* @since 3.2.0
*/
protected function quote($value)
{
// NULL handling
if ($value === null)
{
return 'NULL';
}
// DateTime handling
if ($value instanceof \DateTimeInterface)
{
return $this->db->quote($value->format($this->getDateFormat()));
}
// Native numeric types
if (is_int($value) || is_float($value))
{
return $value;
}
// Stringified numeric values
if (is_string($value) && is_numeric($value))
{
// Case 1: Leading-zero integers like "007"
if ($value[0] === '0' && strlen($value) > 1 && ctype_digit($value))
{
return $this->db->quote($value);
}
// Case 2: Scientific notation - preserve exact format
if (stripos($value, 'e') !== false)
{
return $this->db->quote($value);
}
// Case 3: Decimal float string (not scientific)
if (str_contains($value, '.'))
{
return (float) $value;
}
// Case 4: Pure integer string
if (ctype_digit($value))
{
return (int) $value;
}
}
// Boolean handling
if (is_bool($value))
{
return $value ? 'TRUE' : 'FALSE';
}
// Everything else
return $this->db->quote($value);
}
/**
* Set a table name, adding the
* core component as needed

View File

@ -55,6 +55,8 @@ class Infusion extends Interpretation
*/
public $removeSiteEditFolder = true;
public $secondRunAdmin;
/**
* Constructor
*/

View File

@ -63,8 +63,17 @@ class Sql
*/
public function set(object &$item)
{
if (isset($item->add_sql) && (int) $item->add_sql === 1 && isset($item->source))
if (isset($item->add_sql) && (int) $item->add_sql === 1
&& isset($item->source) && isset($item->name_single_code))
{
// avoid setting this a multiple time for the same name_singe_code
if ((int) $item->source === 1 && isset($this->dispenser->hub['sql'])
&& is_array($this->dispenser->hub['sql'])
&& isset($this->dispenser->hub['sql'][$item->name_single_code]))
{
return;
}
if ((int) $item->source === 1 && isset($item->tables) &&
($string = $this->dump->get(
$item->tables, $item->name_single_code, $item->guid

View File

@ -12,11 +12,11 @@
namespace VDM\Joomla\Componentbuilder\Compiler\Model;
use Joomla\CMS\Factory;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface as JoomlaDatabase;
use VDM\Joomla\Componentbuilder\Compiler\Registry;
use VDM\Joomla\Utilities\ArrayHelper;
use VDM\Joomla\Database\QuoteTrait;
use VDM\Joomla\Componentbuilder\Compiler\Utilities\Placefix;
use VDM\Joomla\Utilities\StringHelper;
/**
@ -26,6 +26,13 @@ use VDM\Joomla\Utilities\StringHelper;
*/
class Sqldump
{
/**
* Function to quote values
*
* @since 5.1.1
*/
use QuoteTrait;
/**
* The compiler registry
*
@ -37,251 +44,91 @@ class Sqldump
/**
* Database object to query local DB
*
* @var JoomlaDatabase
* @since 3.2.0
**/
protected $db;
protected JoomlaDatabase $db;
/**
* Constructor
*
* @param Registry $registry The compiler registry object.
* @param Registry $registry The compiler registry object.
* @param JoomlaDatabase|null $db The joomla database object.
* @since 3.2.0
*/
public function __construct(Registry $registry)
public function __construct(Registry $registry, ?JoomlaDatabase $db = null)
{
$this->registry = $registry;
$this->db = Factory::getDbo();
$this->db = $db ?: Factory::getContainer()->get(JoomlaDatabase::class);
}
/**
* Get SQL Dump
* Generate SQL dump for given view data.
*
* @param array $tables The tables to use in build
* @param string $view The target view/table to dump in
* @param string $view_guid The guid of the target view
* @param array $tables Tables configuration array.
* @param string $view Target view name.
* @param string $viewGuid Unique GUID for view (used in registry path).
*
* @return string|null The data found with the alias
* @since 3.2.0
* @return string|null SQL dump or null on failure.
* @since 3.2.0
*/
public function get(array $tables, string $view, string $view_guid): ?string
public function get(array $tables, string $view, string $viewGuid): ?string
{
// first build a query statement to get all the data (insure it must be added - check the tweaking)
if (ArrayHelper::check($tables)
&& $this->registry-> // default is to add
get('builder.sql_tweak.' . $view_guid . '.add', true))
if (empty($tables) || !$this->shouldBuildDump($viewGuid))
{
$counter = 'a';
return null;
}
// Create a new query object.
$query = $this->db->getQuery(true);
$query = $this->db->getQuery(true);
$runQuery = false;
$alias = 'a';
$fieldsAdded = false;
// switch to only trigger the run of the query if we have tables to query
$run_query = false;
foreach ($tables as $table)
foreach ($tables as $tableConfig)
{
if (empty($tableConfig['table']) || empty($tableConfig['sourcemap']))
{
if (isset($table['table']))
continue;
}
$fieldMappings = $this->parseFieldMappings($tableConfig['sourcemap'], $alias);
if ($alias === 'a')
{
if (!empty($fieldMappings['select']))
{
if ($counter === 'a')
{
// the main table fields
if (strpos((string) $table['sourcemap'], PHP_EOL) !== false)
{
$fields = explode(PHP_EOL, (string) $table['sourcemap']);
if (ArrayHelper::check($fields))
{
// reset array buckets
$sourceArray = [];
$targetArray = [];
foreach ($fields as $field)
{
if (strpos($field, "=>") !== false)
{
list($source, $target) = explode(
"=>", $field
);
$sourceArray[] = $counter . '.' . trim(
$source
);
$targetArray[] = trim($target);
}
}
if (ArrayHelper::check(
$sourceArray
)
&& ArrayHelper::check(
$targetArray
))
{
// add to query
$query->select(
$this->db->quoteName(
$sourceArray, $targetArray
)
);
$query->from(
'#__' . $table['table'] . ' AS a'
);
$run_query = true;
}
// we may need to filter the selection
if (($ids_ = $this->registry->
get('builder.sql_tweak.' . $view_guid . '.where', null)) !== null)
{
// add to query the where filter
$query->where(
'a.id IN (' . $ids_ . ')'
);
}
}
}
}
else
{
// the other tables
if (strpos((string) $table['sourcemap'], PHP_EOL) !== false)
{
$fields = explode(PHP_EOL, (string) $table['sourcemap']);
if (ArrayHelper::check($fields))
{
// reset array buckets
$sourceArray = [];
$targetArray = [];
foreach ($fields as $field)
{
if (strpos($field, "=>") !== false)
{
list($source, $target) = explode(
"=>", $field
);
$sourceArray[] = $counter . '.' . trim(
$source
);
$targetArray[] = trim($target);
}
if (strpos($field, "==") !== false)
{
list($aKey, $bKey) = explode(
"==", $field
);
// add to query
$query->join(
'LEFT', $this->db->quoteName(
'#__' . $table['table'],
$counter
) . ' ON (' . $this->db->quoteName(
'a.' . trim($aKey)
) . ' = ' . $this->db->quoteName(
$counter . '.' . trim($bKey)
) . ')'
);
}
}
if (ArrayHelper::check(
$sourceArray
)
&& ArrayHelper::check(
$targetArray
))
{
// add to query
$query->select(
$this->db->quoteName(
$sourceArray, $targetArray
)
);
}
}
}
}
$counter++;
$query->select($this->db->quoteName($fieldMappings['select'], $fieldMappings['alias']));
$query->from($this->db->quoteName('#__' . $tableConfig['table'], $alias));
$this->applyWhereFilter($query, $viewGuid);
$fieldsAdded = true;
$runQuery = true;
}
else
}
else
{
$this->applyJoins($query, $tableConfig['table'], $alias, $fieldMappings['joins']);
if (!empty($fieldMappings['select']))
{
// see where
// var_dump($view, $view_guid);
// exit;
$query->select($this->db->quoteName($fieldMappings['select'], $fieldMappings['alias']));
$fieldsAdded = true;
}
}
// check if we should run query
if ($run_query)
{
try{
// now get the data
$this->db->setQuery($query);
$this->db->execute();
if ($this->db->getNumRows())
{
// get the data
$data = $this->db->loadObjectList();
$alias++;
}
// start building the MySql dump
$dump = "--";
$dump .= PHP_EOL . "-- Dumping data for table `#__"
. Placefix::_("component") . "_" . $view
. "`";
$dump .= PHP_EOL . "--";
$dump .= PHP_EOL . PHP_EOL . "INSERT INTO `#__" . Placefix::_("component") . "_" . $view . "` (";
foreach ($data as $line)
{
$comaSet = 0;
foreach ($line as $fieldName => $fieldValue)
{
if ($comaSet == 0)
{
$dump .= $this->db->quoteName($fieldName);
}
else
{
$dump .= ", " . $this->db->quoteName(
$fieldName
);
}
$comaSet++;
}
break;
}
$dump .= ") VALUES";
$coma = 0;
foreach ($data as $line)
{
if ($coma == 0)
{
$dump .= PHP_EOL . "(";
}
else
{
$dump .= "," . PHP_EOL . "(";
}
$comaSet = 0;
foreach ($line as $fieldName => $fieldValue)
{
if ($comaSet == 0)
{
$dump .= $this->escape($fieldValue);
}
else
{
$dump .= ", " . $this->escape(
$fieldValue
);
}
$comaSet++;
}
$dump .= ")";
$coma++;
}
$dump .= ";";
if ($runQuery && $fieldsAdded)
{
try {
$this->db->setQuery($query)->execute();
// return build dump query
return $dump;
}
} catch (\Throwable $e) {
// see where
// var_dump($view, $view_guid);
// exit;
if ($this->db->getNumRows())
{
$data = $this->db->loadObjectList();
return $this->buildSqlDump($view, $data);
}
} catch (\Throwable $e) {
// Log or handle exception if needed
}
}
@ -289,35 +136,141 @@ class Sqldump
}
/**
* Escape the values for a SQL dump
* Determine if a dump should be built.
*
* @param string|array $value the value to escape
* @param string $viewGuid
*
* @return string|array on success with escaped string
* @since 3.2.0
* @return bool
* @since 5.1.1
*/
protected function shouldBuildDump(string $viewGuid): bool
{
return (bool) $this->registry->get("builder.sql_tweak.{$viewGuid}.add", true);
}
/**
* Apply optional WHERE clause if set in registry.
*
* @param $query
* @param string $viewGuid
*
* @return void
* @since 5.1.1
*/
protected function applyWhereFilter($query, string $viewGuid): void
{
if ($ids = $this->registry->get("builder.sql_tweak.{$viewGuid}.where"))
{
$query->where("a.id IN ({$ids})");
}
}
/**
* Parse sourcemap lines into SELECT and JOIN definitions.
*
* @param string $map
* @param string $alias
*
* @return array{select: string[], alias: string[], joins: array<int, array{from: string, to: string}>}
* @since 5.1.1
*/
protected function parseFieldMappings(string $map, string $alias): array
{
$lines = explode(PHP_EOL, trim($map));
$select = [];
$aliasFields = [];
$joins = [];
foreach ($lines as $line)
{
$line = trim($line);
if (str_contains($line, '=>'))
{
[$from, $to] = array_map('trim', explode('=>', $line));
$select[] = "{$alias}.{$from}";
$aliasFields[] = $to;
}
elseif (str_contains($line, '=='))
{
[$left, $right] = array_map('trim', explode('==', $line));
$joins[] = ['from' => $left, 'to' => $right];
}
}
return [
'select' => $select,
'alias' => $aliasFields,
'joins' => $joins,
];
}
/**
* Apply JOINs to the query.
*
* @param $query
* @param string $table
* @param string $alias
* @param array $joins
*
* @return void
* @since 5.1.1
*/
protected function applyJoins($query, string $table, string $alias, array $joins): void
{
foreach ($joins as $join)
{
$query->join(
'LEFT',
$this->db->quoteName("#__{$table}", $alias) . ' ON (' .
$this->db->quoteName("a.{$join['from']}") . ' = ' .
$this->db->quoteName("{$alias}.{$join['to']}") . ')'
);
}
}
/**
* Build the SQL INSERT DUMP statement from data.
*
* @param string $view
* @param array<object> $data
*
* @return string
* @since 5.1.1
*/
protected function buildSqlDump(string $view, array $data): string
{
$tableName = "#__" . Placefix::_("component") . "_{$view}";
$fields = array_keys((array) $data[0]);
$header = "--\n-- Dumping data for table `{$tableName}`\n--\n";
$insert = "INSERT INTO `{$tableName}` (" . implode(', ', array_map([$this->db, 'quoteName'], $fields)) . ") VALUES\n";
$rows = array_map(function ($row)
{
$values = array_map([$this, 'escape'], (array) $row);
return '(' . implode(', ', $values) . ')';
}, $data);
return $header . $insert . implode(",\n", $rows) . ";";
}
/**
* Escape SQL value for safe dump using strict quoting rules.
*
* @param mixed $value The value to escape.
*
* @return mixed Escaped SQL-safe literal or quoted string.
* @since 3.2.0
*/
protected function escape($value)
{
// if array then return mapped
if (ArrayHelper::check($value))
if (is_array($value))
{
return array_map(__METHOD__, $value);
return implode(', ', array_map([$this, 'escape'], $value));
}
// if string make sure it is correctly escaped
if (StringHelper::check($value) && !is_numeric($value))
{
return $this->db->quote($value);
}
// if empty value return place holder
if (empty($value))
{
return "''";
}
// if not array or string then return number
return $value;
return $this->quote($value);
}
}

View File

@ -15,7 +15,6 @@ namespace VDM\Joomla\Componentbuilder\Compiler\Model;
use VDM\Joomla\Componentbuilder\Compiler\Registry;
use VDM\Joomla\Utilities\JsonHelper;
use VDM\Joomla\Utilities\ArrayHelper;
use VDM\Joomla\Utilities\ObjectHelper;
/**
@ -63,117 +62,101 @@ class Sqltweaking
if (ArrayHelper::check($item->sql_tweak))
{
// build the tweak settings
$this->tweak(
array_map(
fn($array) => array_map(
function ($value) {
if (!ArrayHelper::check($value)
&& !ObjectHelper::check(
$value
)
&& strval($value) === strval(
intval($value)
))
{
return $value;
}
return $value;
}, $array
), array_values($item->sql_tweak)
)
);
$this->tweak($item->sql_tweak);
}
unset($item->sql_tweak);
}
/**
* To limit the SQL Demo data build in the views
* Limit the SQL Demo data build in the views by applying tweak settings.
*
* @param array $settings Tweaking array.
* @param array $settings The tweak configuration array.
*
* @return void
* @since 3.2.0
* @since 3.2.0
*/
protected function tweak($settings)
protected function tweak(array $settings): void
{
if (ArrayHelper::check($settings))
if (!ArrayHelper::check($settings))
{
foreach ($settings as $setting)
return;
}
foreach ($settings as $setting)
{
$adminView = $setting['adminview'] ?? null;
if (!$adminView)
{
// should sql dump be added
if (1 == $setting['add_sql'])
continue;
}
$addSql = (int) ($setting['add_sql'] ?? 0);
$addSqlOptions = (int) ($setting['add_sql_options'] ?? 0);
if ($addSql === 1 && $addSqlOptions === 2)
{
$ids = $setting['ids'] ?? '';
$idArray = $this->normalizeIds($ids);
if (!empty($idArray))
{
// add sql (by option)
if (2 == $setting['add_sql_options'])
{
// rest always
$id_array = [];
// by id (first remove backups)
$ids = $setting['ids'];
// now get the ids
if (strpos((string) $ids, ',') !== false)
{
$id_array = (array) array_map(
'trim', explode(',', (string) $ids)
);
}
else
{
$id_array[] = trim((string) $ids);
}
$id_array_new = [];
// check for ranges
foreach ($id_array as $key => $id)
{
if (strpos($id, '=>') !== false)
{
$id_range = (array) array_map(
'trim', explode('=>', $id)
);
unset($id_array[$key]);
// build range
if (count((array) $id_range) == 2)
{
$range = range(
$id_range[0], $id_range[1]
);
$id_array_new = [...$id_array_new, ...$range];
}
}
}
if (ArrayHelper::check($id_array_new))
{
$id_array = [...$id_array_new, ...$id_array];
}
// final fixing to array
if (ArrayHelper::check($id_array))
{
// unique
$id_array = array_unique($id_array, SORT_NUMERIC);
// sort
sort($id_array, SORT_NUMERIC);
// now set it to global
$this->registry->
set('builder.sql_tweak.' . $setting['adminview'] . '.where', implode(',', $id_array));
}
}
}
else
{
// do not add sql dump options
$this->registry->
set('builder.sql_tweak.' . $setting['adminview'] . '.add', false);
$this->registry->set(
'builder.sql_tweak.' . $adminView . '.where',
implode(',', $idArray)
);
}
}
elseif ($addSql === 0)
{
$this->registry->set(
'builder.sql_tweak.' . $adminView . '.add',
false
);
}
}
}
/**
* Normalize a comma-separated string of IDs or ID ranges into a unique, sorted array.
*
* Supports individual IDs (e.g., "1,3,5") and ranges (e.g., "10 => 12").
*
* @param string $ids Raw ID string from settings.
*
* @return array<int> Normalized list of numeric IDs.
* @since 5.1.1
*/
private function normalizeIds(string $ids): array
{
$rawIds = array_map('trim', explode(',', $ids));
$finalIds = [];
foreach ($rawIds as $id)
{
if (strpos($id, '=>') !== false)
{
$rangeParts = array_map('trim', explode('=>', $id));
if (count($rangeParts) === 2 && is_numeric($rangeParts[0]) && is_numeric($rangeParts[1]))
{
$range = range((int) $rangeParts[0], (int) $rangeParts[1]);
$finalIds = array_merge($finalIds, $range);
continue;
}
}
if (is_numeric($id))
{
$finalIds[] = (int) $id;
}
}
$finalIds = array_unique($finalIds, SORT_NUMERIC);
sort($finalIds, SORT_NUMERIC);
return $finalIds;
}
}

View File

@ -0,0 +1,120 @@
<?php
/**
* @package Joomla.Component.Builder
*
* @created 4th September, 2022
* @author Llewellyn van der Merwe <https://dev.vdm.io>
* @git Joomla Component Builder <https://git.vdm.dev/joomla/Component-Builder>
* @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace VDM\Joomla\Database;
/**
* Database Quote Trait
*
* @since 5.1.1
*/
trait QuoteTrait
{
/**
* Date format to return
*
* @var string
* @since 5.0.2
*/
protected string $dateFormat = 'Y-m-d H:i:s';
/**
* Safely quote a value for database use, preserving data integrity.
*
* - Native ints/floats passed as-is
* - Clean integer strings are cast to int
* - Clean float strings are cast to float
* - Scientific notation is quoted to preserve original form
* - Leading-zero integers are quoted
* - Dates are formatted and quoted
* - Booleans are converted to TRUE/FALSE
* - Null is converted to NULL
* - All else is quoted with Joomla's db quote
*
* @param mixed $value The value to quote.
*
* @return mixed
* @since 3.2.0
*/
protected function quote($value)
{
// NULL handling
if ($value === null)
{
return 'NULL';
}
// DateTime handling
if ($value instanceof \DateTimeInterface)
{
return $this->db->quote($value->format($this->getDateFormat()));
}
// Native numeric types
if (is_int($value) || is_float($value))
{
return $value;
}
// Stringified numeric values
if (is_string($value) && is_numeric($value))
{
// Case 1: Leading-zero integers like "007"
if ($value[0] === '0' && strlen($value) > 1 && ctype_digit($value))
{
return $this->db->quote($value);
}
// Case 2: Scientific notation - preserve exact format
if (stripos($value, 'e') !== false)
{
return $this->db->quote($value);
}
// Case 3: Decimal float string (not scientific)
if (str_contains($value, '.'))
{
return (float) $value;
}
// Case 4: Pure integer string
if (ctype_digit($value))
{
return (int) $value;
}
}
// Boolean handling
if (is_bool($value))
{
return $value ? 'TRUE' : 'FALSE';
}
// Everything else
return $this->db->quote($value);
}
/**
* Get the date format used for SQL dumps.
*
* This format is used when quoting DateTimeInterface values
* to ensure consistent formatting in INSERT statements.
*
* @return string The SQL-compatible date format.
* @since 5.0.2
*/
protected function getDateFormat(): string
{
return $this->dateFormat;
}
}