v2.4.4: DatabaseEntity migration and bugfixes
This commit is contained in:
parent
b96d0d053c
commit
037f0fae91
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user