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 { public function _execute(): bool {
$key = $this->getParam("key"); $key = $this->getParam("key");
$sql = $this->context->getSQL(); $sql = $this->context->getSQL();
$siteSettings = $this->context->getSettings();
$settings = Settings::getAll($sql, $key, $this->isExternalCall()); $settings = Settings::getAll($sql, $key, $this->isExternalCall());
if ($settings !== null) { if ($settings !== null) {

@ -3,12 +3,24 @@
namespace Core\Configuration; namespace Core\Configuration;
use Core\API\Request; use Core\API\Request;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Query\CreateTable;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
use PHPUnit\Util\Exception; use PHPUnit\Util\Exception;
class CreateDatabase { 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 { public static function createQueries(SQL $sql): array {
$queries = 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 {
$persistables = []; $createdTables = $sql->listTables();
$baseDirs = ["Core", "Site"];
foreach ($baseDirs as $baseDir) { if ($createdTables !== null) {
$entityDirectory = "./$baseDir/Objects/DatabaseEntity/"; foreach ($queries as $query) {
if (file_exists($entityDirectory) && is_dir($entityDirectory)) { if ($query instanceof CreateTable) {
$scan_arr = scandir($entityDirectory); $tableName = $query->getTableName();
$files_arr = array_diff($scan_arr, array('.', '..')); if (!in_array($tableName, $createdTables)) {
foreach ($files_arr as $file) { $createdTables[] = $tableName;
$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)) {
$method = "$className::getHandler";
$handler = call_user_func($method, $sql, null, true);
$persistables[$handler->getTableName()] = $handler;
foreach ($handler->getNMRelations() as $nmTableName => $nmRelation) {
$persistables[$nmTableName] = $nmRelation;
}
}
} }
} }
} }
} 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 = [];
foreach ($entityClasses as $className) {
$reflectionClass = new \ReflectionClass($className);
if ($reflectionClass->isSubclassOf(DatabaseEntity::class)) {
$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); $tableCount = count($persistables);
$createdTables = [];
while (!empty($persistables)) { while (!empty($persistables)) {
$prevCount = $tableCount; $prevCount = $tableCount;
$unmetDependenciesTotal = []; $unmetDependenciesTotal = [];
@ -88,7 +119,7 @@ class CreateDatabase {
$dependsOn = $persistable->dependsOn(); $dependsOn = $persistable->dependsOn();
$unmetDependencies = array_diff($dependsOn, $createdTables); $unmetDependencies = array_diff($dependsOn, $createdTables);
if (empty($unmetDependencies)) { if (empty($unmetDependencies)) {
$queries = array_merge($queries, $persistable->getCreateQueries($sql)); $queries = array_merge($queries, $persistable->getCreateQueries($sql, $skipExisting));
$createdTables[] = $tableName; $createdTables[] = $tableName;
unset($persistables[$tableName]); unset($persistables[$tableName]);
} else { } 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 { public static function loadDefaultACL(SQL $sql, array &$queries): void {
$query = $sql->insert("ApiPermission", ["method", "groups", "description", "is_core"]); $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 { 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) { if ($pattern) {
$query->where(new CondRegex(new Column("name"), $pattern)); $query->where(new CondRegex(new Column("name"), $pattern));

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

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

@ -375,7 +375,10 @@ class MySQL extends SQL {
list ($name, $alias) = $parts; list ($name, $alias) = $parts;
return "`$name` $alias"; return "`$name` $alias";
} else { } 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; 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 { class RowIteratorMySQL extends RowIterator {

@ -334,7 +334,10 @@ class PostgreSQL extends SQL {
list ($name, $alias) = $parts; list ($name, $alias) = $parts;
return "\"$name\" $alias"; return "\"$name\" $alias";
} else { } 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; 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 { class RowIteratorPostgreSQL extends RowIterator {

@ -2,7 +2,10 @@
namespace Core\Driver\SQL\Query; namespace Core\Driver\SQL\Query;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
use Exception;
class CreateTrigger extends Query { class CreateTrigger extends Query {
@ -11,6 +14,9 @@ class CreateTrigger extends Query {
private string $event; private string $event;
private string $tableName; private string $tableName;
private array $parameters; private array $parameters;
private bool $ifNotExist;
private ?CreateProcedure $procedure; private ?CreateProcedure $procedure;
public function __construct(SQL $sql, string $triggerName) { public function __construct(SQL $sql, string $triggerName) {
@ -21,6 +27,7 @@ class CreateTrigger extends Query {
$this->event = ""; $this->event = "";
$this->parameters = []; $this->parameters = [];
$this->procedure = null; $this->procedure = null;
$this->ifNotExist = false;
} }
public function before(): CreateTrigger { public function before(): CreateTrigger {
@ -28,6 +35,11 @@ class CreateTrigger extends Query {
return $this; return $this;
} }
public function onlyIfNotExist(): CreateTrigger {
$this->ifNotExist = true;
return $this;
}
public function after(): CreateTrigger { public function after(): CreateTrigger {
$this->time = "AFTER"; $this->time = "AFTER";
return $this; return $this;
@ -70,7 +82,20 @@ class CreateTrigger extends Query {
$tableName = $this->sql->tableName($this->getTable()); $tableName = $this->sql->tableName($this->getTable());
$params = array(); $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); $triggerBody = $this->sql->createTriggerBody($this, $this->parameters);
if ($triggerBody === null) { if ($triggerBody === null) {
return null; return null;

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

@ -125,10 +125,14 @@ class Context {
public function parseCookies(): void { public function parseCookies(): void {
if ($this->sql) { $settings = $this->getSettings();
if (isset($_COOKIE['session']) && is_string($_COOKIE['session']) && !empty($_COOKIE['session'])) { if (!$settings->isInstalled()) {
$this->loadSession($_COOKIE['session']); // 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 // set language by priority: 1. GET parameter, 2. cookie, 3. user's settings, 4. accept-language header

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

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

@ -1,5 +1,7 @@
<?php <?php
namespace {
use Core\API\Parameter\Parameter; use Core\API\Parameter\Parameter;
use Core\Driver\SQL\Query\CreateTable; use Core\Driver\SQL\Query\CreateTable;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
@ -100,6 +102,28 @@ class DatabaseEntityTest extends \PHPUnit\Framework\TestCase {
public function testDropTable() { public function testDropTable() {
$this->assertTrue(self::$SQL->drop(self::$HANDLER->getTableName())->execute()); $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 { class TestEntity extends DatabaseEntity {
@ -110,3 +134,15 @@ class TestEntity extends DatabaseEntity {
public \DateTime $e; public \DateTime $e;
public ?int $f; public ?int $f;
} }
class TestEntityInherit extends DatabaseEntity {
public TestEntity $rel;
}
}
namespace OverrideNameSpace {
class TestEntityInherit extends \TestEntityInherit {
public int $new;
}
}