v2.4.4: DatabaseEntity migration and bugfixes

This commit is contained in:
Roman 2024-05-21 12:32:44 +02:00
parent b96d0d053c
commit 037f0fae91
14 changed files with 249 additions and 47 deletions

@ -63,7 +63,6 @@ namespace Core\API\Settings {
public function _execute(): bool {
$key = $this->getParam("key");
$sql = $this->context->getSQL();
$siteSettings = $this->context->getSettings();
$settings = Settings::getAll($sql, $key, $this->isExternalCall());
if ($settings !== null) {

@ -3,12 +3,24 @@
namespace Core\Configuration;
use Core\API\Request;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Query\CreateTable;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
use PHPUnit\Util\Exception;
class CreateDatabase {
private static ?Logger $logger = null;
public static function getLogger(SQL $sql): Logger {
if (self::$logger === null) {
self::$logger = new Logger("CreateDatabase", $sql);
}
return self::$logger;
}
public static function createQueries(SQL $sql): array {
$queries = array();
@ -51,35 +63,54 @@ class CreateDatabase {
}
}
private static function loadEntities(SQL $sql, array &$queries): void {
private static function getCreatedTables(SQL $sql, array $queries): ?array {
$createdTables = $sql->listTables();
if ($createdTables !== null) {
foreach ($queries as $query) {
if ($query instanceof CreateTable) {
$tableName = $query->getTableName();
if (!in_array($tableName, $createdTables)) {
$createdTables[] = $tableName;
}
}
}
} else {
self::getLogger($sql)->warning("Error querying existing tables: " . $sql->getLastError());
}
return $createdTables;
}
public static function createEntityQueries(SQL $sql, array $entityClasses, array &$queries, bool $skipExisting = false): void {
if (empty($entityClasses)) {
return;
}
// first, check what tables are already created
$createdTables = self::getCreatedTables($sql, $queries);
if ($createdTables === null) {
throw new \Exception("Error querying existing tables");
}
// then collect all persistable entities (tables, relations, etc.)
$persistables = [];
$baseDirs = ["Core", "Site"];
foreach ($baseDirs as $baseDir) {
$entityDirectory = "./$baseDir/Objects/DatabaseEntity/";
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
$scan_arr = scandir($entityDirectory);
$files_arr = array_diff($scan_arr, array('.', '..'));
foreach ($files_arr as $file) {
$suffix = ".class.php";
if (endsWith($file, $suffix)) {
$className = substr($file, 0, strlen($file) - strlen($suffix));
$className = "\\$baseDir\\Objects\\DatabaseEntity\\$className";
foreach ($entityClasses as $className) {
$reflectionClass = new \ReflectionClass($className);
if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) {
$method = "$className::getHandler";
$handler = call_user_func($method, $sql, null, true);
$handler = ("$className::getHandler")($sql, null, true);
$persistables[$handler->getTableName()] = $handler;
foreach ($handler->getNMRelations() as $nmTableName => $nmRelation) {
$persistables[$nmTableName] = $nmRelation;
}
}
}
}
} else {
throw new \Exception("Class '$className' is not a subclass of DatabaseEntity");
}
}
// now order the persistable entities so all dependencies are met.
$tableCount = count($persistables);
$createdTables = [];
while (!empty($persistables)) {
$prevCount = $tableCount;
$unmetDependenciesTotal = [];
@ -88,7 +119,7 @@ class CreateDatabase {
$dependsOn = $persistable->dependsOn();
$unmetDependencies = array_diff($dependsOn, $createdTables);
if (empty($unmetDependencies)) {
$queries = array_merge($queries, $persistable->getCreateQueries($sql));
$queries = array_merge($queries, $persistable->getCreateQueries($sql, $skipExisting));
$createdTables[] = $tableName;
unset($persistables[$tableName]);
} else {
@ -104,6 +135,32 @@ class CreateDatabase {
}
}
private static function loadEntities(SQL $sql, array &$queries): void {
$classes = [];
$baseDirs = ["Core", "Site"];
foreach ($baseDirs as $baseDir) {
$entityDirectory = "./$baseDir/Objects/DatabaseEntity/";
if (file_exists($entityDirectory) && is_dir($entityDirectory)) {
$scan_arr = scandir($entityDirectory);
$files_arr = array_diff($scan_arr, [".", ".."]);
foreach ($files_arr as $file) {
$suffix = ".class.php";
if (endsWith($file, $suffix)) {
$className = substr($file, 0, strlen($file) - strlen($suffix));
$className = "\\$baseDir\\Objects\\DatabaseEntity\\$className";
$reflectionClass = new \ReflectionClass($className);
if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) {
$classes[] = $className;
}
}
}
}
}
self::createEntityQueries($sql, $classes, $queries);
}
public static function loadDefaultACL(SQL $sql, array &$queries): void {
$query = $sql->insert("ApiPermission", ["method", "groups", "description", "is_core"]);

@ -62,7 +62,14 @@ class Settings {
}
public static function getAll(?SQL $sql, ?string $pattern = null, bool $external = false): ?array {
$query = $sql->select("name", "value")->from("Settings");
// We do not have a Settings table yet, we might still be in installation phase
if (!$sql->tableExists("Settings")) {
return null;
}
$query = $sql->select("name", "value")
->from("Settings");
if ($pattern) {
$query->where(new CondRegex(new Column("name"), $pattern));

@ -19,7 +19,6 @@ namespace Documents\Install {
use Core\Configuration\Configuration;
use Core\Configuration\CreateDatabase;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\SQL;
use Core\Elements\Body;
use Core\Elements\Head;
@ -187,7 +186,7 @@ namespace Documents\Install {
}
$sql = $context->getSQL();
if (!$sql || !$sql->isConnected()) {
if (!$sql || !$sql->isConnected() || !$sql->tableExists(User::getHandler($sql)->getTableName())) {
return self::DATABASE_CONFIGURATION;
}
@ -439,9 +438,12 @@ namespace Documents\Install {
$context = $this->getDocument()->getContext();
if ($this->getParameter("prev") === "true") {
// TODO: drop the previous database here?
/*
$success = $context->getConfig()->delete("\\Site\\Configuration\\Database");
$msg = $success ? "" : error_get_last();
return ["success" => $success, "msg" => $msg];
*/
return ["success" => false, "msg" => "Cannot revert this installation step."];
}
$username = $this->getParameter("username");
@ -755,7 +757,7 @@ namespace Documents\Install {
["title" => "Password", "name" => "password", "type" => "password", "required" => true],
["title" => "Confirm Password", "name" => "confirmPassword", "type" => "password", "required" => true],
],
"previousButton" => true
"previousButton" => false,
],
self::ADD_MAIL_SERVICE => [
"title" => "Optional: Add Mail Service",

@ -6,7 +6,7 @@ use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;
use Core\External\PHPMailer\Exception;
use Exception;
class DateAdd extends Expression {

@ -6,7 +6,7 @@ use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;
use Core\External\PHPMailer\Exception;
use Exception;
class DateSub extends Expression {

@ -375,7 +375,10 @@ class MySQL extends SQL {
list ($name, $alias) = $parts;
return "`$name` $alias";
} else {
return "`$table`";
$parts = explode(".", $table);
return implode(".", array_map(function ($n) {
return "`$n`";
}, $parts));
}
}
}
@ -484,6 +487,27 @@ class MySQL extends SQL {
return $res && $res[0]["count"] > 0;
}
public function listTables(): ?array {
$tableSchema = $this->connectionData->getProperty("database");
$res = $this->select("TABLE_NAME")
->from("information_schema.TABLES")
->where(new Compare("TABLE_SCHEMA", $tableSchema, "=", true))
->where(new CondLike(new Column("TABLE_TYPE"), "BASE TABLE"))
->execute();
if ($res !== false) {
$tableNames = [];
foreach ($res as $row) {
$tableNames[] = $row["TABLE_NAME"];
}
return $tableNames;
}
return null;
}
}
class RowIteratorMySQL extends RowIterator {

@ -334,7 +334,10 @@ class PostgreSQL extends SQL {
list ($name, $alias) = $parts;
return "\"$name\" $alias";
} else {
return "\"$table\"";
$parts = explode(".", $table);
return implode(".", array_map(function ($n) {
return "\"$n\"";
}, $parts));
}
}
}
@ -463,6 +466,28 @@ class PostgreSQL extends SQL {
return $res && $res[0]["count"] > 0;
}
public function listTables(): ?array {
$tableSchema = $this->connectionData->getProperty("database");
$res = $this->select("tablename")
->from("pg_tables")
->where(new Compare("schemaname", $tableSchema))
->execute();
if ($res !== false) {
$tableNames = [];
foreach ($res as $row) {
$tableNames[] = $row["tablename"];
}
return $tableNames;
}
return null;
}
}
class RowIteratorPostgreSQL extends RowIterator {

@ -2,7 +2,10 @@
namespace Core\Driver\SQL\Query;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;
use Exception;
class CreateTrigger extends Query {
@ -11,6 +14,9 @@ class CreateTrigger extends Query {
private string $event;
private string $tableName;
private array $parameters;
private bool $ifNotExist;
private ?CreateProcedure $procedure;
public function __construct(SQL $sql, string $triggerName) {
@ -21,6 +27,7 @@ class CreateTrigger extends Query {
$this->event = "";
$this->parameters = [];
$this->procedure = null;
$this->ifNotExist = false;
}
public function before(): CreateTrigger {
@ -28,6 +35,11 @@ class CreateTrigger extends Query {
return $this;
}
public function onlyIfNotExist(): CreateTrigger {
$this->ifNotExist = true;
return $this;
}
public function after(): CreateTrigger {
$this->time = "AFTER";
return $this;
@ -70,7 +82,20 @@ class CreateTrigger extends Query {
$tableName = $this->sql->tableName($this->getTable());
$params = array();
$query = "CREATE TRIGGER $name $time $event ON $tableName FOR EACH ROW ";
if ($this->sql instanceof MySQL) {
$query = "CREATE TRIGGER";
if ($this->ifNotExist) {
$query .= " IF NOT EXISTS";
}
} else if ($this->sql instanceof PostgreSQL) {
$ifNotExists = $this->ifNotExist ? " OR REPLACE" : "";
$query = "CREATE$ifNotExists TRIGGER";
} else {
throw new Exception("CreateTrigger Not implemented for driver type: " . get_class($this->sql));
}
$query .= " $name $time $event ON $tableName FOR EACH ROW ";
$triggerBody = $this->sql->createTriggerBody($this, $this->parameters);
if ($triggerBody === null) {
return null;

@ -130,6 +130,8 @@ abstract class SQL {
// Schema
public abstract function tableExists(string $tableName): bool;
public abstract function listTables(): ?array;
/**
* @param Query $query
* @param int $fetchType

@ -125,11 +125,15 @@ class Context {
public function parseCookies(): void {
if ($this->sql) {
$settings = $this->getSettings();
if (!$settings->isInstalled()) {
// we cannot process user sessions or localization yet.
return;
}
if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) {
$this->loadSession($_COOKIE['session']);
}
}
// set language by priority: 1. GET parameter, 2. cookie, 3. user's settings, 4. accept-language header
if (isset($_GET['lang']) && is_string($_GET["lang"]) && !empty($_GET["lang"])) {

@ -713,13 +713,13 @@ class DatabaseEntityHandler implements Persistable {
return $res;
}
public function getCreateQueries(SQL $sql): array {
public function getCreateQueries(SQL $sql, bool $canExist = false): array {
$queries = [];
$table = $this->getTableName();
// Create Table
$queries[] = $this->getTableQuery($sql);
$queries[] = $this->getTableQuery($sql, $canExist);
// pre defined values
$getPredefinedValues = $this->entityClass->getMethod("getPredefinedValues");
@ -733,43 +733,64 @@ class DatabaseEntityHandler implements Persistable {
$entityLogConfig = $entityLogConfig->getValue();
if (isset($entityLogConfig["insert"]) && $entityLogConfig["insert"] === true) {
$queries[] = $sql->createTrigger("${table}_trg_insert")
$trigger = $sql->createTrigger("${table}_trg_insert")
->after()->insert($table)
->exec(new CreateProcedure($sql, "InsertEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("id"),
"lifetime" => $entityLogConfig["lifetime"] ?? 90,
]);
if ($canExist) {
$trigger->onlyIfNotExist();
}
$queries[] = $trigger;
}
if (isset($entityLogConfig["update"]) && $entityLogConfig["update"] === true) {
$queries[] = $sql->createTrigger("${table}_trg_update")
$trigger = $sql->createTrigger("${table}_trg_update")
->after()->update($table)
->exec(new CreateProcedure($sql, "UpdateEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("id"),
]);
if ($canExist) {
$trigger->onlyIfNotExist();
}
$queries[] = $trigger;
}
if (isset($entityLogConfig["delete"]) && $entityLogConfig["delete"] === true) {
$queries[] = $sql->createTrigger("${table}_trg_delete")
$trigger = $sql->createTrigger("${table}_trg_delete")
->after()->delete($table)
->exec(new CreateProcedure($sql, "DeleteEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("id"),
]);
if ($canExist) {
$trigger->onlyIfNotExist();
}
$queries[] = $trigger;
}
return $queries;
}
public function getTableQuery(SQL $sql): CreateTable {
public function getTableQuery(SQL $sql, bool $canExist = false): CreateTable {
$query = $sql->createTable($this->tableName)
->onlyIfNotExists()
->addSerial("id")
->primaryKey("id");
if ($canExist) {
$query->onlyIfNotExists();
}
foreach ($this->columns as $column) {
$query->addColumn($column);
}

@ -10,7 +10,7 @@ if (is_file($autoLoad)) {
require_once $autoLoad;
}
const WEBBASE_VERSION = "2.4.3";
const WEBBASE_VERSION = "2.4.4";
spl_autoload_extensions(".php");
spl_autoload_register(function ($class) {

@ -1,5 +1,7 @@
<?php
namespace {
use Core\API\Parameter\Parameter;
use Core\Driver\SQL\Query\CreateTable;
use Core\Driver\SQL\SQL;
@ -100,6 +102,28 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase {
public function testDropTable() {
$this->assertTrue(self::$SQL->drop(self::$HANDLER->getTableName())->execute());
}
public function testTableNames() {
$sql = self::$SQL;
$this->assertEquals("TestEntity", TestEntity::getHandler($sql, null, true)->getTableName());
$this->assertEquals("TestEntityInherit", TestEntityInherit::getHandler($sql, null, true)->getTableName());
$this->assertEquals("TestEntityInherit", OverrideNameSpace\TestEntityInherit::getHandler($sql, null, true)->getTableName());
}
public function testCreateQueries() {
$queries = [];
$entities = [TestEntity::class, TestEntityInherit::class, OverrideNameSpace\TestEntityInherit::class];
\Core\Configuration\CreateDatabase::createEntityQueries(self::$SQL, $entities, $queries);
$this->assertCount(2, $queries);
$tables = [];
foreach ($queries as $query) {
$this->assertInstanceOf(CreateTable::class, $query);
$tables[] = $query->getTableName();
}
$this->assertEquals(["TestEntity", "TestEntityInherit"], $tables);
}
}
class TestEntity extends DatabaseEntity {
@ -110,3 +134,15 @@ class TestEntity extends DatabaseEntity {
public \DateTime $e;
public ?int $f;
}
class TestEntityInherit extends DatabaseEntity {
public TestEntity $rel;
}
}
namespace OverrideNameSpace {
class TestEntityInherit extends \TestEntityInherit {
public int $new;
}
}