SQL expression rewrite, Pagination, some frontend stuff

This commit is contained in:
2023-01-05 22:47:17 +01:00
parent 4bfd6754cf
commit 99bfd7e505
61 changed files with 1745 additions and 570 deletions

View File

@@ -2,6 +2,7 @@
namespace Core\API {
use Core\Driver\SQL\Expression\Count;
use Core\Objects\Context;
abstract class GroupsAPI extends Request {
@@ -12,7 +13,7 @@ namespace Core\API {
protected function groupExists($name): bool {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count())
$res = $sql->select(new Count())
->from("Group")
->whereEq("name", $name)
->execute();
@@ -29,76 +30,81 @@ namespace Core\API\Groups {
use Core\API\GroupsAPI;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\API\Traits\Pagination;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Expression\Alias;
use Core\Driver\SQL\Expression\Count;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Controller\NMRelation;
use Core\Objects\DatabaseEntity\Group;
class Fetch extends GroupsAPI {
use Pagination;
private int $groupCount;
public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, array(
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
));
parent::__construct($context, $externalCall,
self::getPaginationParameters(['id', 'name', 'member_count'])
);
$this->groupCount = 0;
}
public function _execute(): bool {
$page = $this->getParam("page");
if($page < 1) {
return $this->createError("Invalid page count");
}
$count = $this->getParam("count");
if($count < 1 || $count > 50) {
return $this->createError("Invalid fetch count");
}
$sql = $this->context->getSQL();
$groupCount = Group::count($sql);
if ($groupCount === false) {
return $this->createError("Error fetching group count: " . $sql->getLastError());
if (!$this->initPagination($sql, Group::class)) {
return false;
}
$groups = Group::findBy(Group::createBuilder($sql, false)
->orderBy("id")
->ascending()
->limit($count)
->offset(($page - 1) * $count));
$memberCount = new Alias($sql->select(new Count())
->from(NMRelation::buildTableName("User", "Group"))
->whereEq("group_id", new Column("Group.id")), "memberCount");
if ($groups !== false) {
$groupsQuery = $this->createPaginationQuery($sql, [$memberCount]);
$groups = $groupsQuery->execute();
if ($groups !== false && $groups !== null) {
$this->result["groups"] = [];
$this->result["pageCount"] = intval(ceil($this->groupCount / $count));
$this->result["totalCount"] = $this->groupCount;
foreach ($groups as $groupId => $group) {
$this->result["groups"][$groupId] = $group->jsonSerialize();
$this->result["groups"][$groupId]["memberCount"] = 0;
}
$nmTable = NMRelation::buildTableName("User", "Group");
$res = $sql->select("group_id", $sql->count("user_id"))
->from($nmTable)
->groupBy("group_id")
->execute();
if (is_array($res)) {
foreach ($res as $row) {
list ($groupId, $memberCount) = [$row["group_id"], $row["user_id_count"]];
if (isset($this->result["groups"][$groupId])) {
$this->result["groups"][$groupId]["memberCount"] = $memberCount;
}
}
foreach ($groups as $group) {
$groupData = $group->jsonSerialize();
$groupData["memberCount"] = $group["memberCount"];
$this->result["groups"][] = $groupData;
}
} else {
return $this->createError("Error fetching groups: " . $sql->getLastError());
}
return $this->success;
}
}
class Get extends GroupsAPI {
public function __construct(Context $context, bool $externalCall = false) {
parent::__construct($context, $externalCall, [
"id" => new Parameter("id", Parameter::TYPE_INT)
]);
}
protected function _execute(): bool {
$sql = $this->context->getSQL();
$groupId = $this->getParam("id");
$group = Group::find($sql, $groupId);
if ($group === false) {
return $this->createError("Error fetching group: " . $sql->getLastError());
} else if ($group === null) {
return $this->createError("Group not found");
} else {
$this->result["group"] = $group->jsonSerialize();
$this->result["group"]["members"] = $group->getMembers($sql);
}
return true;
}
}
class Create extends GroupsAPI {
public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, array(

View File

@@ -5,22 +5,22 @@ namespace Core\API\Parameter;
use DateTime;
class Parameter {
const TYPE_INT = 0;
const TYPE_FLOAT = 1;
const TYPE_BOOLEAN = 2;
const TYPE_STRING = 3;
const TYPE_DATE = 4;
const TYPE_TIME = 5;
const TYPE_INT = 0;
const TYPE_FLOAT = 1;
const TYPE_BOOLEAN = 2;
const TYPE_STRING = 3;
const TYPE_DATE = 4;
const TYPE_TIME = 5;
const TYPE_DATE_TIME = 6;
const TYPE_EMAIL = 7;
const TYPE_EMAIL = 7;
// only internal access
const TYPE_RAW = 8;
const TYPE_RAW = 8;
// only json will work here I guess
// nope. also name[]=value
const TYPE_ARRAY = 9;
const TYPE_MIXED = 10;
const TYPE_ARRAY = 9;
const TYPE_MIXED = 10;
const names = array('Integer', 'Float', 'Boolean', 'String', 'Date', 'Time', 'DateTime', 'E-Mail', 'Raw', 'Array', 'Mixed');
@@ -35,13 +35,15 @@ class Parameter {
public bool $optional;
public int $type;
public string $typeName;
public ?array $choices;
public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL) {
public function __construct(string $name, int $type, bool $optional = FALSE, $defaultValue = NULL, ?array $choices = NULL) {
$this->name = $name;
$this->optional = $optional;
$this->defaultValue = $defaultValue;
$this->value = $defaultValue;
$this->type = $type;
$this->choices = $choices;
$this->typeName = $this->getTypeName();
}
@@ -63,22 +65,22 @@ class Parameter {
}
public function getSwaggerFormat(): ?string {
switch ($this->type) {
case self::TYPE_DATE:
return self::DATE_FORMAT;
case self::TYPE_TIME:
return self::TIME_FORMAT;
case self::TYPE_DATE_TIME:
return self::DATE_TIME_FORMAT;
case self::TYPE_EMAIL:
return "email";
default:
return null;
}
return match ($this->type) {
self::TYPE_DATE => self::DATE_FORMAT,
self::TYPE_TIME => self::TIME_FORMAT,
self::TYPE_DATE_TIME => self::DATE_TIME_FORMAT,
self::TYPE_EMAIL => "email",
default => null,
};
}
public function getTypeName(): string {
return ($this->type >= 0 && $this->type < count(Parameter::names)) ? Parameter::names[$this->type] : "INVALID";
$typeName = Parameter::names[$this->type] ?? "INVALID";
if ($this->choices) {
$typeName .= ", choices: " . json_encode($this->choices);
}
return $typeName;
}
public function toString(): string {
@@ -86,7 +88,7 @@ class Parameter {
$str = "$typeName $this->name";
$defaultValue = (is_null($this->value) ? 'NULL' : $this->value);
if($this->optional) {
if ($this->optional) {
$str = "[$str = $defaultValue]";
}
@@ -94,21 +96,21 @@ class Parameter {
}
public static function parseType($value): int {
if(is_array($value))
if (is_array($value))
return Parameter::TYPE_ARRAY;
else if(is_numeric($value) && intval($value) == $value)
else if (is_numeric($value) && intval($value) == $value)
return Parameter::TYPE_INT;
else if(is_float($value) || (is_numeric($value) && floatval($value) == $value))
else if (is_float($value) || (is_numeric($value) && floatval($value) == $value))
return Parameter::TYPE_FLOAT;
else if(is_bool($value) || $value == "true" || $value == "false")
else if (is_bool($value) || $value == "true" || $value == "false")
return Parameter::TYPE_BOOLEAN;
else if(is_a($value, 'DateTime'))
else if (is_a($value, 'DateTime'))
return Parameter::TYPE_DATE_TIME;
else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value)
else if ($value !== null && ($d = DateTime::createFromFormat(self::DATE_FORMAT, $value)) && $d->format(self::DATE_FORMAT) === $value)
return Parameter::TYPE_DATE;
else if($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value)
else if ($value !== null && ($d = DateTime::createFromFormat(self::TIME_FORMAT, $value)) && $d->format(self::TIME_FORMAT) === $value)
return Parameter::TYPE_TIME;
else if($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value)
else if ($value !== null && ($d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value)) && $d->format(self::DATE_TIME_FORMAT) === $value)
return Parameter::TYPE_DATE_TIME;
else if (filter_var($value, FILTER_VALIDATE_EMAIL))
return Parameter::TYPE_EMAIL;
@@ -117,88 +119,90 @@ class Parameter {
}
public function parseParam($value): bool {
switch($this->type) {
$valid = false;
switch ($this->type) {
case Parameter::TYPE_INT:
if(is_numeric($value) && intval($value) == $value) {
if (is_numeric($value) && intval($value) == $value) {
$this->value = intval($value);
return true;
$valid = true;
}
return false;
break;
case Parameter::TYPE_FLOAT:
if(is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) {
if (is_numeric($value) && (floatval($value) == $value || intval($value) == $value)) {
$this->value = floatval($value);
return true;
$valid = true;
}
return false;
break;
case Parameter::TYPE_BOOLEAN:
if(strcasecmp($value, 'true') === 0)
if (strcasecmp($value, 'true') === 0) {
$this->value = true;
else if(strcasecmp($value, 'false') === 0)
$valid = true;
} else if (strcasecmp($value, 'false') === 0) {
$this->value = false;
else if(is_bool($value))
$valid = true;
} else if (is_bool($value)) {
$this->value = (bool)$value;
else
return false;
return true;
case Parameter::TYPE_DATE:
if(is_a($value, "DateTime")) {
$this->value = $value;
return true;
$valid = true;
}
$d = DateTime::createFromFormat(self::DATE_FORMAT, $value);
if($d && $d->format(self::DATE_FORMAT) === $value) {
$this->value = $d;
return true;
}
return false;
break;
case Parameter::TYPE_TIME:
if(is_a($value, "DateTime")) {
$this->value = $value;
return true;
}
$d = DateTime::createFromFormat(self::TIME_FORMAT, $value);
if($d && $d->format(self::TIME_FORMAT) === $value) {
$this->value = $d;
return true;
}
return false;
case Parameter::TYPE_DATE:
case Parameter::TYPE_DATE_TIME:
if(is_a($value, 'DateTime')) {
if ($value instanceof DateTime) {
$this->value = $value;
return true;
$valid = true;
} else {
$d = DateTime::createFromFormat(self::DATE_TIME_FORMAT, $value);
if($d && $d->format(self::DATE_TIME_FORMAT) === $value) {
$format = $this->getFormat();
$d = DateTime::createFromFormat($format, $value);
if ($d && $d->format($format) === $value) {
$this->value = $d;
return true;
$valid = true;
}
}
return false;
break;
case Parameter::TYPE_EMAIL:
if (filter_var($value, FILTER_VALIDATE_EMAIL)) {
$this->value = $value;
return true;
$valid = true;
}
return false;
break;
case Parameter::TYPE_ARRAY:
if(is_array($value)) {
if (is_array($value)) {
$this->value = $value;
return true;
$valid = true;
}
return false;
break;
default:
$this->value = $value;
return true;
$valid = true;
break;
}
if ($valid && $this->choices) {
if (!in_array($this->value, $this->choices)) {
return false;
}
}
return $valid;
}
private function getFormat(): ?string {
if ($this->type === self::TYPE_TIME) {
return self::TIME_FORMAT;
} else if ($this->type === self::TYPE_DATE) {
return self::DATE_FORMAT;
} else if ($this->type === self::TYPE_DATE_TIME) {
return self::DATE_TIME_FORMAT;
} else {
return null;
}
}
}

View File

@@ -5,9 +5,9 @@ namespace Core\API\Parameter;
class StringType extends Parameter {
public int $maxLength;
public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL) {
public function __construct(string $name, int $maxLength = -1, bool $optional = FALSE, ?string $defaultValue = NULL, ?array $choices = NULL) {
$this->maxLength = $maxLength;
parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue);
parent::__construct($name, Parameter::TYPE_STRING, $optional, $defaultValue, $choices);
}
public function parseParam($value): bool {

View File

@@ -2,6 +2,8 @@
namespace Core\API;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\Expression\Distinct;
use DateTime;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondBool;
@@ -16,24 +18,24 @@ class Stats extends Request {
parent::__construct($context, $externalCall, array());
}
private function getUserCount() {
private function getUserCount(): int {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count())->from("User")->execute();
$res = $sql->select(new Count())->from("User")->execute();
$this->success = $this->success && ($res !== FALSE);
$this->lastError = $sql->getLastError();
return ($this->success ? $res[0]["count"] : 0);
return ($this->success ? intval($res[0]["count"]) : 0);
}
private function getPageCount() {
private function getPageCount(): int {
$sql = $this->context->getSQL();
$res = $sql->select($sql->count())->from("Route")
$res = $sql->select(new Count())->from("Route")
->where(new CondBool("active"))
->execute();
$this->success = $this->success && ($res !== FALSE);
$this->lastError = $sql->getLastError();
return ($this->success ? $res[0]["count"] : 0);
return ($this->success ? intval($res[0]["count"]) : 0);
}
private function checkSettings(): bool {
@@ -55,7 +57,7 @@ class Stats extends Request {
$date = new DateTime();
$monthStart = $date->format("Ym00");
$monthEnd = $date->modify("+1 month")->format("Ym00");
$res = $sql->select($sql->count($sql->distinct("cookie")))
$res = $sql->select(new Count(new Distinct("cookie")))
->from("Visitor")
->where(new Compare("day", $monthStart, ">="))
->where(new Compare("day", $monthEnd, "<"))
@@ -92,19 +94,22 @@ class Stats extends Request {
return false;
}
$this->result["userCount"] = $userCount;
$this->result["pageCount"] = $pageCount;
$this->result["visitors"] = $visitorStatistics;
$this->result["visitorsTotal"] = $visitorCount;
$this->result["server"] = array(
"version" => WEBBASE_VERSION,
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
"memory_usage" => memory_get_usage(),
"load_avg" => $loadAvg,
"database" => $this->context->getSQL()->getStatus(),
"mail" => $this->mailConfigured,
"reCaptcha" => $this->recaptchaConfigured
);
$this->result["data"] = [
"userCount" => $userCount,
"pageCount" => $pageCount,
"visitors" => $visitorStatistics,
"visitorsTotal" => $visitorCount,
"server" => [
"version" => WEBBASE_VERSION,
"server" => $_SERVER["SERVER_SOFTWARE"] ?? "Unknown",
"memory_usage" => memory_get_usage(),
"load_avg" => $loadAvg,
"database" => $this->context->getSQL()->getStatus(),
"mail" => $this->mailConfigured,
"reCaptcha" => $this->recaptchaConfigured
],
];
return $this->success;
}

View File

@@ -72,7 +72,7 @@ namespace Core\API\Template {
try {
$this->result["html"] = $twigEnvironment->render($templateFile, $parameters);
} catch (LoaderError | RuntimeError | SyntaxError $e) {
} catch (LoaderError | RuntimeError | SyntaxError | \RuntimeException $e) {
return $this->createError("Error rendering twig template: " . $e->getMessage());
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Core\API\Traits;
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityQuery;
use Core\Objects\DatabaseEntity\User;
trait Pagination {
static function getPaginationParameters(array $orderColumns): array {
return [
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20),
'orderBy' => new StringType('orderBy', -1, true, "id", $orderColumns),
'sortOrder' => new StringType('sortOrder', -1, true, 'asc', ['asc', 'desc']),
];
}
function initPagination(SQL $sql, string $class, ?Condition $condition = null, int $maxPageSize = 100): bool {
$this->paginationClass = $class;
$this->paginationCondition = $condition;
if (!$this->validateParameters($maxPageSize)) {
return false;
}
$this->entityCount = call_user_func("$this->paginationClass::count", $sql, $condition);
if ($this->entityCount === false) {
return $this->createError("Error fetching $this->paginationClass::count: " . $sql->getLastError());
}
$pageCount = intval(ceil($this->entityCount / $this->pageSize));
$this->page = min($this->page, $pageCount); // number of pages changed due to pageSize / filter
$this->result["pagination"] = [
"current" => $this->page,
"pageSize" => $this->pageSize,
"pageCount" => $pageCount,
"total" => $this->entityCount
];
return true;
}
function validateParameters(int $maxCount = 100): bool {
$this->page = $this->getParam("page");
if ($this->page < 1) {
return $this->createError("Invalid page count");
}
$this->pageSize = $this->getParam("count");
if ($this->pageSize < 1 || $this->pageSize > $maxCount) {
return $this->createError("Invalid fetch count");
}
return true;
}
function createPaginationQuery(SQL $sql, array $additionalValues = []): DatabaseEntityQuery {
$page = $this->getParam("page");
$count = $this->getParam("count");
$orderBy = $this->getParam("orderBy");
$sortOrder = $this->getParam("sortOrder");
$baseQuery = call_user_func("$this->paginationClass::createBuilder", $sql, false);
$entityQuery = $baseQuery
->fetchEntities()
->limit($count)
->offset(($page - 1) * $count);
if ($this->paginationCondition) {
$entityQuery->where($this->paginationCondition);
}
if (!empty($additionalValues)) {
foreach ($additionalValues as $additionalValue) {
$entityQuery->addCustomValue($additionalValue);
}
}
if ($orderBy) {
$handler = $baseQuery->getHandler();
$baseTable = $handler->getTableName();
$sortColumn = DatabaseEntityHandler::getColumnName($orderBy);
$fullyQualifiedColumn = "$baseTable.$sortColumn";
$selectedColumns = $baseQuery->getSelectValues();
if (in_array($sortColumn, $selectedColumns)) {
$entityQuery->orderBy($sortColumn);
} else if (in_array($fullyQualifiedColumn, $selectedColumns)) {
$entityQuery->orderBy($fullyQualifiedColumn);
} else {
$entityQuery->orderBy($orderBy);
}
}
if ($sortOrder === "asc") {
$entityQuery->ascending();
} else {
$entityQuery->descending();
}
return $entityQuery;
}
}

View File

@@ -132,11 +132,12 @@ namespace Core\API\User {
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\API\Template\Render;
use Core\API\Traits\Pagination;
use Core\API\UserAPI;
use Core\API\VerifyCaptcha;
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Expression\Alias;
use Core\Objects\DatabaseEntity\Group;
use Core\Objects\DatabaseEntity\UserToken;
use Core\Driver\SQL\Column\Column;
@@ -209,96 +210,65 @@ namespace Core\API\User {
class Fetch extends UserAPI {
use Pagination;
public function __construct(Context $context, $externalCall = false) {
parent::__construct($context, $externalCall, array(
'page' => new Parameter('page', Parameter::TYPE_INT, true, 1),
'count' => new Parameter('count', Parameter::TYPE_INT, true, 20)
));
}
private function selectIds($page, $count): array|bool {
$sql = $this->context->getSQL();
$res = $sql->select("User.id")
->from("User")
->limit($count)
->offset(($page - 1) * $count)
->orderBy("User.id")
->ascending()
->execute();
$this->success = ($res !== NULL);
$this->lastError = $sql->getLastError();
if ($this->success && is_array($res)) {
return array_map(function ($row) {
return intval($row["id"]);
}, $res);
}
return false;
parent::__construct($context, $externalCall,
self::getPaginationParameters(['id', 'name', 'email', 'groups', 'registeredAt'])
);
}
public function _execute(): bool {
$page = $this->getParam("page");
if ($page < 1) {
return $this->createError("Invalid page count");
}
$count = $this->getParam("count");
if ($count < 1 || $count > 50) {
return $this->createError("Invalid fetch count");
}
$condition = null;
$currentUser = $this->context->getUser();
$fullInfo = ($currentUser->hasGroup(Group::ADMIN) ||
$currentUser->hasGroup(Group::SUPPORT));
$currentUser->hasGroup(Group::SUPPORT));
$orderBy = $this->getParam("orderBy");
$publicAttributes = ["id", "name", "fullName", "profilePicture", "email"]; // TODO: , "groupNames"];
$condition = null;
if (!$fullInfo) {
$condition = new CondOr(
new Compare("User.id", $currentUser->getId()),
new CondBool("User.confirmed")
);
if ($orderBy && !in_array($orderBy, $publicAttributes)) {
return $this->createError("Insufficient permissions for sorting by field '$orderBy'");
}
}
$sql = $this->context->getSQL();
$userCount = User::count($sql, $condition);
if ($userCount === false) {
return $this->createError("Error fetching user count: " . $sql->getLastError());
if (!$this->initPagination($sql, User::class, $condition)) {
return false;
}
$userQuery = User::createBuilder($sql, false)
->orderBy("id")
->ascending()
->limit($count)
->offset(($page - 1) * $count)
->fetchEntities();
if ($condition) {
$userQuery->where($condition);
}
$groupNames = new Alias(
$sql->select(new JsonArrayAgg("name"))->from("Group")
->leftJoin("NM_Group_User", "NM_Group_User.group_id", "Group.id")
->whereEq("NM_Group_User.user_id", new Column("User.id")),
"groups"
);
$userQuery = $this->createPaginationQuery($sql, [$groupNames]);
$users = User::findBy($userQuery);
if ($users !== false) {
if ($users !== false && $users !== null) {
$this->result["users"] = [];
foreach ($users as $userId => $user) {
$serialized = $user->jsonSerialize();
if (!$fullInfo && $userId !== $currentUser->getId()) {
$publicAttributes = ["id", "name", "fullName", "profilePicture", "email", "groups"];
foreach (array_keys($serialized) as $attr) {
if (!in_array($attr, $publicAttributes)) {
unset($serialized[$attr]);
unset ($serialized[$attr]);
}
}
}
$this->result["users"][$userId] = $serialized;
$this->result["users"][] = $serialized;
}
$this->result["pageCount"] = intval(ceil($userCount / $count));
$this->result["totalCount"] = $userCount;
} else {
return $this->createError("Error fetching users: " . $sql->getLastError());
}

View File

@@ -16,6 +16,7 @@ namespace Core\API\Visitors {
use Core\API\Parameter\Parameter;
use Core\API\Parameter\StringType;
use Core\API\VisitorsAPI;
use Core\Driver\SQL\Expression\Count;
use DateTime;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Expression\Add;
@@ -82,7 +83,7 @@ namespace Core\API\Visitors {
$type = $this->getParam("type");
$sql = $this->context->getSQL();
$query = $sql->select($sql->count(), "day")
$query = $sql->select(new Count(), "day")
->from("Visitor")
->whereGt("count", 1)
->groupBy("day")

View File

@@ -19,6 +19,7 @@ namespace Documents\Install {
use Core\Configuration\Configuration;
use Core\Configuration\CreateDatabase;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\Query\Commit;
use Core\Driver\SQL\Query\StartTransaction;
use Core\Driver\SQL\SQL;
@@ -196,8 +197,7 @@ namespace Documents\Install {
return self::DATABASE_CONFIGURATION;
}
$countKeyword = $sql->count();
$res = $sql->select($countKeyword)->from("User")->execute();
$res = $sql->select(new Count())->from("User")->execute();
if ($res === FALSE) {
return self::DATABASE_CONFIGURATION;
} else {

View File

@@ -3,6 +3,7 @@
namespace Core\Driver\SQL\Column;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\SQL;
class Column extends Expression {
@@ -20,4 +21,7 @@ class Column extends Expression {
public function notNull(): bool { return !$this->nullable; }
public function getDefaultValue() { return $this->defaultValue; }
function getExpression(SQL $sql, array &$params): string {
return $sql->columnName($this->name);
}
}

View File

@@ -2,6 +2,8 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
class Compare extends Condition {
private string $operator;
@@ -18,4 +20,16 @@ class Compare extends Condition {
public function getValue() { return $this->value; }
public function getOperator(): string { return $this->operator; }
function getExpression(SQL $sql, array &$params): string {
if ($this->value === null) {
if ($this->operator === "=") {
return $sql->columnName($this->column) . " IS NULL";
} else if ($this->operator === "!=") {
return $sql->columnName($this->column) . " IS NOT NULL";
}
}
return $sql->columnName($this->column) . $this->operator . $sql->addValue($this->value, $params);
}
}

View File

@@ -2,6 +2,8 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
class CondAnd extends Condition {
private array $conditions;
@@ -11,4 +13,12 @@ class CondAnd extends Condition {
}
public function getConditions(): array { return $this->conditions; }
function getExpression(SQL $sql, array &$params): string {
$conditions = array();
foreach($this->getConditions() as $cond) {
$conditions[] = $sql->addValue($cond, $params);
}
return "(" . implode(" AND ", $conditions) . ")";
}
}

View File

@@ -2,6 +2,8 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
class CondBool extends Condition {
private $value;
@@ -12,4 +14,11 @@ class CondBool extends Condition {
public function getValue() { return $this->value; }
function getExpression(SQL $sql, array &$params): string {
if (is_string($this->value)) {
return $sql->columnName($this->value);
} else {
return $sql->addValue($this->value);
}
}
}

View File

@@ -2,6 +2,9 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\SQL;
class CondIn extends Condition {
private $needle;
@@ -14,4 +17,25 @@ class CondIn extends Condition {
public function getNeedle() { return $this->needle; }
public function getHaystack() { return $this->haystack; }
function getExpression(SQL $sql, array &$params): string {
$haystack = $this->getHaystack();
if (is_array($haystack)) {
$values = array();
foreach ($haystack as $value) {
$values[] = $sql->addValue($value, $params);
}
$values = implode(",", $values);
$values = "($values)";
} else if($haystack instanceof Select) {
$values = $haystack->build($params);
} else {
$sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack));
return false;
}
return $sql->addValue($this->needle, $params) . " IN $values";
}
}

View File

@@ -2,6 +2,8 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
abstract class CondKeyword extends Condition {
private $leftExpression;
@@ -17,4 +19,11 @@ abstract class CondKeyword extends Condition {
public function getLeftExp() { return $this->leftExpression; }
public function getRightExp() { return $this->rightExpression; }
public function getKeyword(): string { return $this->keyword; }
function getExpression(SQL $sql, array &$params): string {
$keyword = $this->getKeyword();
$left = $sql->addValue($this->getLeftExp(), $params);
$right = $sql->addValue($this->getRightExp(), $params);
return "$left $keyword $right";
}
}

View File

@@ -7,4 +7,5 @@ class CondLike extends CondKeyword {
public function __construct($leftExpression, $rightExpression) {
parent::__construct("LIKE", $leftExpression, $rightExpression);
}
}

View File

@@ -2,15 +2,21 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
class CondNot extends Condition {
private $expression; // string or condition
private mixed $expression; // string or condition
public function __construct($expression) {
public function __construct(mixed $expression) {
$this->expression = $expression;
}
public function getExpression() {
return $this->expression;
public function getExpression(SQL $sql, array &$params): string {
if (is_string($this->expression)) {
return "NOT " . $sql->columnName($this->expression);
} else {
return "NOT " . $sql->addValue($this->expression, $params);
}
}
}

View File

@@ -2,6 +2,8 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
class CondNull extends Condition {
private string $column;
@@ -11,4 +13,8 @@ class CondNull extends Condition {
}
public function getColumn(): string { return $this->column; }
function getExpression(SQL $sql, array &$params): string {
return $sql->columnName($this->getColumn()) . " IS NULL";
}
}

View File

@@ -2,6 +2,8 @@
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\SQL;
class CondOr extends Condition {
private array $conditions;
@@ -11,4 +13,12 @@ class CondOr extends Condition {
}
public function getConditions(): array { return $this->conditions; }
function getExpression(SQL $sql, array &$params): string {
$conditions = array();
foreach($this->getConditions() as $cond) {
$conditions[] = $sql->addValue($cond, $params);
}
return "(" . implode(" OR ", $conditions) . ")";
}
}

View File

@@ -1,22 +1,23 @@
<?php
namespace Core\Driver\SQL\Condition;
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\SQL;
class Exists extends Condition {
class Exists extends Condition
{
private Select $subQuery;
public function __construct(Select $subQuery)
{
public function __construct(Select $subQuery) {
$this->subQuery = $subQuery;
}
public function getSubQuery(): Select
{
public function getSubQuery(): Select {
return $this->subQuery;
}
function getExpression(SQL $sql, array &$params): string {
return "EXISTS(" .$this->getSubQuery()->build($params) . ")";
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
class Alias extends Expression {
private mixed $value;
private string $alias;
public function __construct(mixed $value, string $alias) {
$this->value = $value;
$this->alias = $alias;
}
public function getAlias(): string {
return $this->alias;
}
public function getValue(): mixed {
return $this->value;
}
protected function addValue(SQL $sql, array &$params): string {
return $sql->addValue($this->value, $params);
}
public function getExpression(SQL $sql, array &$params): string {
return $this->addValue($sql, $params) . " AS " . $this->getAlias();
}
}

View File

@@ -3,6 +3,7 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\SQL;
class CaseWhen extends Expression {
@@ -20,4 +21,13 @@ class CaseWhen extends Expression {
public function getTrueCase() { return $this->trueCase; }
public function getFalseCase() { return $this->falseCase; }
function getExpression(SQL $sql, array &$params): string {
$condition = $sql->buildCondition($this->getCondition(), $params);
// psql requires constant values here
$trueCase = $sql->addValue($this->getTrueCase(), $params, true);
$falseCase = $sql->addValue($this->getFalseCase(), $params, true);
return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END";
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
class Count extends Alias {
public function __construct(mixed $value = "*", string $alias = "count") {
parent::__construct($value, $alias);
}
function addValue(SQL $sql, array &$params): string {
$value = $this->getValue();
if (is_string($value)) {
if ($value === "*") {
return "COUNT(*)";
} else {
return "COUNT(" . $sql->columnName($value) . ")";
}
} else {
return "COUNT(" . $sql->addValue($value, $params) . ")";
}
}
}

View File

@@ -2,6 +2,20 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;
use Exception;
class CurrentTimeStamp extends Expression {
function getExpression(SQL $sql, array &$params): string {
if ($sql instanceof MySQL) {
return "NOW()";
} else if ($sql instanceof PostgreSQL) {
return "CURRENT_TIMESTAMP";
} else {
throw new Exception("CurrentTimeStamp Not implemented for driver type: " . get_class($sql));
}
}
}

View File

@@ -2,6 +2,12 @@
namespace Core\Driver\SQL\Expression;
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;
class DateAdd extends Expression {
private Expression $lhs;
@@ -18,4 +24,26 @@ class DateAdd extends Expression {
public function getRHS(): Expression { return $this->rhs; }
public function getUnit(): string { return $this->unit; }
function getExpression(SQL $sql, array &$params): string {
if ($sql instanceof MySQL) {
$lhs = $sql->addValue($this->getLHS(), $params);
$rhs = $sql->addValue($this->getRHS(), $params);
$unit = $this->getUnit();
return "DATE_ADD($lhs, INTERVAL $rhs $unit)";
} else if ($sql instanceof PostgreSQL) {
$lhs = $sql->addValue($this->getLHS(), $params);
$rhs = $sql->addValue($this->getRHS(), $params);
$unit = $this->getUnit();
if ($this->getRHS() instanceof Column) {
$rhs = "$rhs * INTERVAL '1 $unit'";
} else {
$rhs = "$rhs $unit";
}
return "$lhs - $rhs";
} else {
throw new Exception("DateAdd Not implemented for driver type: " . get_class($sql));
}
}
}

View File

@@ -2,6 +2,12 @@
namespace Core\Driver\SQL\Expression;
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;
class DateSub extends Expression {
private Expression $lhs;
@@ -18,4 +24,26 @@ class DateSub extends Expression {
public function getRHS(): Expression { return $this->rhs; }
public function getUnit(): string { return $this->unit; }
function getExpression(SQL $sql, array &$params): string {
if ($sql instanceof MySQL) {
$lhs = $sql->addValue($this->getLHS(), $params);
$rhs = $sql->addValue($this->getRHS(), $params);
$unit = $this->getUnit();
return "DATE_SUB($lhs, INTERVAL $rhs $unit)";
} else if ($sql instanceof PostgreSQL) {
$lhs = $sql->addValue($this->getLHS(), $params);
$rhs = $sql->addValue($this->getRHS(), $params);
$unit = $this->getUnit();
if ($this->getRHS() instanceof Column) {
$rhs = "$rhs * INTERVAL '1 $unit'";
} else {
$rhs = "$rhs $unit";
}
return "$lhs - $rhs";
} else {
throw new Exception("DateSub Not implemented for driver type: " . get_class($sql));
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
class Distinct extends Expression {
private mixed $value;
public function __construct(mixed $value) {
$this->value = $value;
}
public function getValue(): mixed {
return $this->value;
}
function getExpression(SQL $sql, array &$params): string {
return "DISTINCT(" . $sql->addValue($this->getValue(), $params) . ")";
}
}

View File

@@ -2,6 +2,10 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\SQL;
abstract class Expression {
abstract function getExpression(SQL $sql, array &$params): string;
}

View File

@@ -2,17 +2,29 @@
namespace Core\Driver\SQL\Expression;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;
use Exception;
class JsonArrayAgg extends Expression {
private $value;
private string $alias;
private mixed $value;
public function __construct($value, string $alias) {
public function __construct(mixed $value) {
$this->value = $value;
$this->alias = $alias;
}
public function getValue() { return $this->value; }
public function getAlias(): string { return $this->alias; }
public function getExpression(SQL $sql, array &$params): string {
$value = is_string($this->value) ? new Column($this->value) : $this->value;
$value = $sql->addValue($value, $params);
if ($sql instanceof MySQL) {
return "JSON_ARRAYAGG($value)";
} else if ($sql instanceof PostgreSQL) {
return "JSON_AGG($value)";
} else {
throw new Exception("JsonArrayAgg not implemented for driver type: " . get_class($sql));
}
}
}

View File

@@ -2,17 +2,15 @@
namespace Core\Driver\SQL\Expression;
class Sum extends Expression {
use Core\Driver\SQL\SQL;
private $value;
private string $alias;
class Sum extends Alias {
public function __construct($value, string $alias) {
$this->value = $value;
$this->alias = $alias;
public function __construct(mixed $value, string $alias) {
parent::__construct($value, $alias);
}
public function getValue() { return $this->value; }
public function getAlias(): string { return $this->alias; }
protected function addValue(SQL $sql, array &$params): string {
return "SUM(" . $sql->addValue($this->getValue(), $params) . ")";
}
}

View File

@@ -4,6 +4,7 @@ namespace Core\Driver\SQL;
use Core\Driver\SQL\Expression\Expression;
// Unsafe sql
class Keyword extends Expression {
private string $value;
@@ -14,4 +15,7 @@ class Keyword extends Expression {
public function getValue(): string { return $this->value; }
function getExpression(SQL $sql, array &$params): string {
return $this->value;
}
}

View File

@@ -17,10 +17,7 @@ use Core\Driver\SQL\Column\JsonColumn;
use Core\Driver\SQL\Expression\Add;
use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Driver\SQL\Expression\DateAdd;
use Core\Driver\SQL\Expression\DateSub;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\Expression\JsonArrayAgg;
use Core\Driver\SQL\Query\CreateProcedure;
use Core\Driver\SQL\Query\CreateTrigger;
use Core\Driver\SQL\Query\Query;
@@ -337,14 +334,8 @@ class MySQL extends SQL {
}
public function addValue($val, &$params = NULL, bool $unsafe = false) {
if ($val instanceof Keyword) {
return $val->getValue();
} else if ($val instanceof CurrentColumn) {
return $val->getName();
} else if ($val instanceof Column) {
return $this->columnName($val->getName());
} else if ($val instanceof Expression) {
return $this->createExpression($val, $params);
if ($val instanceof Expression) {
return $val->getExpression($this, $params);
} else {
if ($unsafe) {
return $this->getUnsafeValue($val);
@@ -460,24 +451,6 @@ class MySQL extends SQL {
return $query;
}
protected function createExpression(Expression $exp, array &$params): ?string {
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
$lhs = $this->addValue($exp->getLHS(), $params);
$rhs = $this->addValue($exp->getRHS(), $params);
$unit = $exp->getUnit();
$dateFunction = ($exp instanceof DateAdd ? "DATE_ADD" : "DATE_SUB");
return "$dateFunction($lhs, INTERVAL $rhs $unit)";
} else if ($exp instanceof CurrentTimeStamp) {
return "NOW()";
} else if ($exp instanceof JsonArrayAgg) {
$value = $this->addValue($exp->getValue(), $params);
$alias = $this->columnName($exp->getAlias());
return "JSON_ARRAYAGG($value) as $alias";
} else {
return parent::createExpression($exp, $params);
}
}
}
class RowIteratorMySQL extends RowIterator {

View File

@@ -17,10 +17,7 @@ use Core\Driver\SQL\Column\JsonColumn;
use Core\Driver\SQL\Condition\CondRegex;
use Core\Driver\SQL\Expression\Add;
use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Driver\SQL\Expression\DateAdd;
use Core\Driver\SQL\Expression\DateSub;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\Expression\JsonArrayAgg;
use Core\Driver\SQL\Query\CreateProcedure;
use Core\Driver\SQL\Query\CreateTrigger;
use Core\Driver\SQL\Query\Insert;
@@ -301,16 +298,13 @@ class PostgreSQL extends SQL {
}
public function addValue($val, &$params = NULL, bool $unsafe = false) {
if ($val instanceof Keyword) {
return $val->getValue();
} else if ($val instanceof CurrentTable) {
// I don't remember we need this here?
/*if ($val instanceof CurrentTable) {
return "TG_TABLE_NAME";
} else if ($val instanceof CurrentColumn) {
return "NEW." . $this->columnName($val->getName());
} else if ($val instanceof Column) {
return $this->columnName($val->getName());
} else if ($val instanceof Expression) {
return $this->createExpression($val, $params);
} else */if ($val instanceof Expression) {
return $val->getExpression($this, $params);
} else {
if ($unsafe) {
return $this->getUnsafeValue($val);
@@ -450,31 +444,6 @@ class PostgreSQL extends SQL {
return $query;
}
protected function createExpression(Expression $exp, array &$params): ?string {
if ($exp instanceof DateAdd || $exp instanceof DateSub) {
$lhs = $this->addValue($exp->getLHS(), $params);
$rhs = $this->addValue($exp->getRHS(), $params);
$unit = $exp->getUnit();
if ($exp->getRHS() instanceof Column) {
$rhs = "$rhs * INTERVAL '1 $unit'";
} else {
$rhs = "$rhs $unit";
}
$operator = ($exp instanceof DateAdd ? "+" : "-");
return "$lhs $operator $rhs";
} else if ($exp instanceof CurrentTimeStamp) {
return "CURRENT_TIMESTAMP";
} else if ($exp instanceof JsonArrayAgg) {
$value = $this->addValue($exp->getValue(), $params);
$alias = $this->columnName($exp->getAlias());
return "JSON_AGG($value) as $alias";
} else {
return parent::createExpression($exp, $params);
}
}
}
class RowIteratorPostgreSQL extends RowIterator {

View File

@@ -26,4 +26,8 @@ abstract class Query extends Expression {
}
public abstract function build(array &$params): ?string;
public function getExpression(SQL $sql, array &$params): string {
return "(" . $this->build($params) . ")";
}
}

View File

@@ -3,6 +3,7 @@
namespace Core\Driver\SQL\Query;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\Expression\JsonArrayAgg;
use Core\Driver\SQL\Join\InnerJoin;
use Core\Driver\SQL\Join\Join;
@@ -38,8 +39,13 @@ class Select extends ConditionalQuery {
$this->fetchType = SQL::FETCH_ALL;
}
public function addColumn(string $columnName): Select {
$this->selectValues[] = $columnName;
public function select(...$selectValues): Select {
$this->selectValues = (!empty($selectValues) && is_array($selectValues[0])) ? $selectValues[0] : $selectValues;
return $this;
}
public function addSelectValue(...$selectValues): Select {
$this->selectValues = array_merge($this->selectValues, $selectValues);
return $this;
}
@@ -142,25 +148,6 @@ class Select extends ConditionalQuery {
foreach ($this->selectValues as $value) {
if (is_string($value)) {
$selectValues[] = $this->sql->columnName($value);
} else if ($value instanceof Select) {
$subSelect = $value->build($params);
if (count($value->getSelectValues()) !== 1) {
$selectValues[] = "($subSelect)";
} else {
$columnAlias = null;
$subSelectColumn = $value->getSelectValues()[0];
if (is_string($subSelectColumn) && ($index = stripos($subSelectColumn, " as ")) !== FALSE) {
$columnAlias = substr($subSelectColumn, $index + 4);
} else if ($subSelectColumn instanceof JsonArrayAgg) {
$columnAlias = $subSelectColumn->getAlias();
}
if ($columnAlias) {
$selectValues[] = "($subSelect) as $columnAlias";
} else {
$selectValues[] = "($subSelect)";
}
}
} else {
$selectValues[] = $this->sql->addValue($value, $params);
}

View File

@@ -4,24 +4,12 @@ namespace Core\Driver\SQL;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Condition\Compare;
use Core\Driver\SQL\Condition\CondAnd;
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\Condition\CondKeyword;
use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\Sql\Condition\CondNull;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Condition\Exists;
use Core\Driver\SQL\Constraint\Constraint;
use Core\Driver\SQL\Constraint\Unique;
use Core\Driver\SQL\Constraint\PrimaryKey;
use Core\Driver\SQL\Constraint\ForeignKey;
use Core\Driver\SQL\Expression\CaseWhen;
use Core\Driver\SQL\Expression\CurrentTimeStamp;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\Expression\Sum;
use Core\Driver\SQL\Query\AlterTable;
use Core\Driver\SQL\Query\Commit;
use Core\Driver\SQL\Query\CreateProcedure;
@@ -236,6 +224,7 @@ abstract class SQL {
public abstract function createTriggerBody(CreateTrigger $trigger, array $params = []): ?string;
public abstract function getProcedureHead(CreateProcedure $procedure): ?string;
public abstract function getColumnType(Column $column): ?string;
public abstract function getStatus();
public function getProcedureTail(): string { return ""; }
public function getReturning(?string $columns): string { return ""; }
@@ -247,6 +236,10 @@ abstract class SQL {
return $statements;
}
public function getLogger(): Logger {
return $this->logger;
}
protected function getUnsafeValue($value): ?string {
if (is_string($value)) {
return "'" . addslashes("$value") . "'"; // unsafe operation here...
@@ -273,60 +266,15 @@ abstract class SQL {
public function now(): CurrentTimeStamp { return new CurrentTimeStamp(); }
public function currentTimestamp(): CurrentTimeStamp { return new CurrentTimeStamp(); }
public function count($col = NULL): Keyword {
if (is_null($col)) {
return new Keyword("COUNT(*) AS count");
} else if($col instanceof Keyword) {
return new Keyword("COUNT(" . $col->getValue() . ") AS count");
} else {
$countCol = strtolower(str_replace(".","_", $col)) . "_count";
$col = $this->columnName($col);
return new Keyword("COUNT($col) AS $countCol");
}
}
public function distinct($col): Keyword {
$col = $this->columnName($col);
return new Keyword("DISTINCT($col)");
}
// Statements
/**
* @return mixed
*/
protected abstract function execute($query, $values = NULL, int $fetchType = self::FETCH_NONE);
public function buildCondition($condition, &$params) {
public function buildCondition(Condition|array $condition, &$params): string {
if ($condition instanceof CondOr) {
$conditions = array();
foreach($condition->getConditions() as $cond) {
$conditions[] = $this->buildCondition($cond, $params);
}
return "(" . implode(" OR ", $conditions) . ")";
} else if ($condition instanceof CondAnd) {
$conditions = array();
foreach($condition->getConditions() as $cond) {
$conditions[] = $this->buildCondition($cond, $params);
}
return "(" . implode(" AND ", $conditions) . ")";
} else if ($condition instanceof Compare) {
$column = $this->columnName($condition->getColumn());
$value = $condition->getValue();
$operator = $condition->getOperator();
if ($value === null) {
if ($operator === "=") {
return "$column IS NULL";
} else if ($operator === "!=") {
return "$column IS NOT NULL";
}
}
return $column . $operator . $this->addValue($value, $params);
} else if ($condition instanceof CondBool) {
return $this->columnName($condition->getValue());
} else if (is_array($condition)) {
if (is_array($condition)) {
if (count($condition) === 1) {
return $this->buildCondition($condition[0], $params);
} else {
@@ -336,77 +284,8 @@ abstract class SQL {
}
return implode(" AND ", $conditions);
}
} else if($condition instanceof CondIn) {
$needle = $condition->getNeedle();
$haystack = $condition->getHaystack();
if (is_array($haystack)) {
$values = array();
foreach ($haystack as $value) {
$values[] = $this->addValue($value, $params);
}
$values = implode(",", $values);
} else if($haystack instanceof Select) {
$values = $haystack->build($params);
} else {
$this->lastError = $this->logger->error("Unsupported in-expression value: " . get_class($condition));
return false;
}
if ($needle instanceof Column) {
$lhs = $this->createExpression($needle, $params);
} else {
$lhs = $this->addValue($needle, $params);
}
return "$lhs IN ($values)";
} else if($condition instanceof CondKeyword) {
$left = $condition->getLeftExp();
$right = $condition->getRightExp();
$keyword = $condition->getKeyword();
$left = ($left instanceof Column) ? $this->columnName($left->getName()) : $this->addValue($left, $params);
$right = ($right instanceof Column) ? $this->columnName($right->getName()) : $this->addValue($right, $params);
return "$left $keyword $right ";
} else if($condition instanceof CondNot) {
$expression = $condition->getExpression();
if ($expression instanceof Condition) {
$expression = $this->buildCondition($expression, $params);
} else {
$expression = $this->columnName($expression);
}
return "NOT $expression";
} else if ($condition instanceof CondNull) {
return $this->columnName($condition->getColumn()) . " IS NULL";
} else if ($condition instanceof Exists) {
return "EXISTS(" .$condition->getSubQuery()->build($params) . ")";
} else {
$this->lastError = $this->logger->error("Unsupported condition type: " . gettype($condition));
return null;
}
}
protected function createExpression(Expression $exp, array &$params): ?string {
if ($exp instanceof Column) {
return $this->columnName($exp->getName());
} else if ($exp instanceof Query) {
return "(" . $exp->build($params) . ")";
} else if ($exp instanceof CaseWhen) {
$condition = $this->buildCondition($exp->getCondition(), $params);
// psql requires constant values here
$trueCase = $this->addValue($exp->getTrueCase(), $params, true);
$falseCase = $this->addValue($exp->getFalseCase(), $params, true);
return "CASE WHEN $condition THEN $trueCase ELSE $falseCase END";
} else if ($exp instanceof Sum) {
$value = $this->addValue($exp->getValue(), $params);
$alias = $this->columnName($exp->getAlias());
return "SUM($value) AS $alias";
} else {
$this->lastError = $this->logger->error("Unsupported expression type: " . get_class($exp));
return null;
return $this->addValue($condition, $params);
}
}
@@ -441,8 +320,6 @@ abstract class SQL {
return $sql;
}
public abstract function getStatus();
public function parseBool($val) : bool {
return in_array($val, array(true, 1, '1', 't', 'true', 'TRUE'), true);
}

View File

@@ -8,7 +8,7 @@ use Core\Driver\SQL\Column\Column;
class CurrentColumn extends Column {
public function __construct(string $string) {
parent::__construct($string);
public function __construct(string $name) {
parent::__construct($name);
}
}

View File

@@ -2,10 +2,23 @@
namespace Core\Driver\SQL\Type;
use Core\Driver\SQL\Column\StringColumn;
use Core\Driver\SQL\Expression\Expression;
use Core\Driver\SQL\MySQL;
use Core\Driver\SQL\PostgreSQL;
use Core\Driver\SQL\SQL;
class CurrentTable extends Expression {
class CurrentTable extends StringColumn {
public function __construct() {
parent::__construct("CURRENT_TABLE");
}
function getExpression(SQL $sql, array &$params): string {
if ($sql instanceof MySQL) {
// CURRENT_TABLE
} else if ($sql instanceof PostgreSQL) {
return "TG_TABLE_NAME";
} else {
}
}
}

View File

@@ -12,4 +12,7 @@ return [
"loading" => "Laden",
"logout" => "Ausloggen",
"noscript" => "Sie müssen Javascript aktivieren um diese Anwendung zu benutzen",
# data table
"showing_x_of_y_entries" => "Zeige %d von %d Einträgen",
];

View File

@@ -12,4 +12,7 @@ return [
"loading" => "Loading",
"logout" => "Logout",
"noscript" => "You need Javascript enabled to run this app",
# data table
"showing_x_of_y_entries" => "Showing %d of %d entries",
];

View File

@@ -2,10 +2,14 @@
namespace Core\Objects\DatabaseEntity\Controller;
use ArrayAccess;
use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\Expression\Count;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\Transient;
use JsonSerializable;
abstract class DatabaseEntity {
abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
protected static array $entityLogConfig = [
"insert" => false,
@@ -16,11 +20,38 @@ abstract class DatabaseEntity {
private static array $handlers = [];
protected ?int $id;
#[Transient] protected array $customData = [];
public function __construct(?int $id = null) {
$this->id = $id;
}
public function offsetExists(mixed $offset): bool {
return property_exists($this, $offset) || array_key_exists($offset, $this->customData);
}
public function offsetGet(mixed $offset): mixed {
if (property_exists($this, $offset)) {
return $this->{$offset};
} else {
return $this->customData[$offset];
}
}
public function offsetSet(mixed $offset, mixed $value): void {
if (property_exists($this, $offset)) {
$this->{$offset} = $value;
} else {
$this->customData[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void {
if (array_key_exists($offset, $this->customData)) {
unset($this->customData[$offset]);
}
}
public abstract function jsonSerialize(): array;
public function preInsert(array &$row) { }
@@ -49,7 +80,7 @@ abstract class DatabaseEntity {
public static function exists(SQL $sql, int $id): bool {
$handler = self::getHandler($sql);
$res = $sql->select($sql->count())
$res = $sql->select(new Count())
->from($handler->getTableName())
->whereEq($handler->getTableName() . ".id", $id)
->execute();
@@ -148,7 +179,7 @@ abstract class DatabaseEntity {
public static function count(SQL $sql, ?Condition $condition = null): int|bool {
$handler = self::getHandler($sql);
$query = $sql->select($sql->count())
$query = $sql->select(new Count())
->from($handler->getTableName());
if ($condition) {

View File

@@ -269,7 +269,7 @@ class DatabaseEntityHandler implements Persistable {
return $rel_row;
}
private function getValueFromRow(array $row, string $propertyName, mixed &$value): bool {
private function getValueFromRow(array $row, string $propertyName, mixed &$value, bool $initEntities = false): bool {
$column = $this->columns[$propertyName] ?? null;
if (!$column) {
return false;
@@ -290,8 +290,12 @@ class DatabaseEntityHandler implements Persistable {
if (array_key_exists($relColumnPrefix . "id", $row)) {
$relId = $row[$relColumnPrefix . "id"];
if ($relId !== null) {
$relationHandler = $this->relations[$propertyName];
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix));
if ($initEntities) {
$relationHandler = $this->relations[$propertyName];
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix), [], true);
} else {
return false;
}
} else if (!$column->notNull()) {
$value = null;
} else {
@@ -305,7 +309,7 @@ class DatabaseEntityHandler implements Persistable {
return true;
}
public function entityFromRow(array $row): ?DatabaseEntity {
public function entityFromRow(array $row, array $additionalColumns = [], bool $initEntities = false): ?DatabaseEntity {
try {
$constructorClass = $this->entityClass;
@@ -324,12 +328,18 @@ class DatabaseEntityHandler implements Persistable {
}
foreach ($this->properties as $property) {
if ($this->getValueFromRow($row, $property->getName(), $value)) {
if ($this->getValueFromRow($row, $property->getName(), $value, $initEntities)) {
$property->setAccessible(true);
$property->setValue($entity, $value);
}
}
foreach ($additionalColumns as $column) {
if (!in_array($column, $this->columns) && !isset($this->properties[$column])) {
$entity[$column] = $row[$column];
}
}
// init n:m / 1:n properties with empty arrays
foreach ($this->nmRelations as $nmRelation) {
foreach ($nmRelation->getProperties($this) as $property) {
@@ -453,9 +463,12 @@ class DatabaseEntityHandler implements Persistable {
if ($recursive) {
foreach ($entities as $entity) {
foreach ($this->relations as $propertyName => $relHandler) {
$relEntity = $this->properties[$propertyName]->getValue($entity);
if ($relEntity) {
$relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true);
$property = $this->properties[$propertyName];
if ($property->isInitialized($entity) || true) {
$relEntity = $this->properties[$propertyName]->getValue($entity);
if ($relEntity) {
$relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true);
}
}
}
}
@@ -483,10 +496,10 @@ class DatabaseEntityHandler implements Persistable {
->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id"))
->where(new CondIn(new Column($thisIdColumn), $entityIds));
$relEntityQuery->addColumn($thisIdColumn);
$relEntityQuery->addSelectValue(new Column($thisIdColumn));
foreach ($dataColumns as $tableDataColumns) {
foreach ($tableDataColumns as $columnName) {
$relEntityQuery->addColumn($columnName);
$relEntityQuery->addSelectValue(new Column($columnName));
}
}
@@ -500,7 +513,7 @@ class DatabaseEntityHandler implements Persistable {
foreach ($rows as $row) {
$relId = $row["id"];
if (!isset($relEntities[$relId])) {
$relEntity = $otherHandler->entityFromRow($row);
$relEntity = $otherHandler->entityFromRow($row, [], $recursive);
$relEntities[$relId] = $relEntity;
}

View File

@@ -3,8 +3,11 @@
namespace Core\Objects\DatabaseEntity\Controller;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Expression\Alias;
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\SQL;
use Core\External\PHPMailer\Exception;
/**
* this class is similar to \Driver\SQL\Query\Select but with reduced functionality
@@ -20,6 +23,7 @@ class DatabaseEntityQuery extends Select {
private DatabaseEntityHandler $handler;
private int $resultType;
private bool $logVerbose;
private array $additionalColumns;
private int $fetchSubEntities;
@@ -29,6 +33,7 @@ class DatabaseEntityQuery extends Select {
$this->logger = new Logger("DB-EntityQuery", $handler->getSQL());
$this->resultType = $resultType;
$this->logVerbose = false;
$this->additionalColumns = [];
$this->from($handler->getTableName());
$this->fetchSubEntities = self::FETCH_NONE;
@@ -37,6 +42,26 @@ class DatabaseEntityQuery extends Select {
}
}
public function addCustomValue(mixed $selectValue): Select {
if (is_string($selectValue)) {
$this->additionalColumns[] = $selectValue;
} else if ($selectValue instanceof Alias) {
$this->additionalColumns[] = $selectValue->getAlias();
} else if ($selectValue instanceof Column) {
$this->additionalColumns[] = $selectValue->getName();
} else {
$this->logger->debug("Cannot get selected column name from custom value of type: " . get_class($selectValue));
return $this;
}
$this->addSelectValue($selectValue);
return $this;
}
public function getHandler(): DatabaseEntityHandler {
return $this->handler;
}
public function debug(): DatabaseEntityQuery {
$this->logVerbose = true;
return $this;
@@ -112,7 +137,7 @@ class DatabaseEntityQuery extends Select {
if ($this->resultType === SQL::FETCH_ALL) {
$entities = [];
foreach ($res as $row) {
$entity = $this->handler->entityFromRow($row);
$entity = $this->handler->entityFromRow($row, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE);
if ($entity) {
$entities[$entity->getId()] = $entity;
}
@@ -124,7 +149,7 @@ class DatabaseEntityQuery extends Select {
return $entities;
} else if ($this->resultType === SQL::FETCH_ONE) {
$entity = $this->handler->entityFromRow($res);
$entity = $this->handler->entityFromRow($res, $this->additionalColumns, $this->fetchSubEntities !== self::FETCH_NONE);
if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) {
$this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities === self::FETCH_RECURSIVE);
}

View File

@@ -104,6 +104,8 @@ class NMRelation implements Persistable {
}
public static function buildTableName(string ...$tables): string {
// in case of class passed here
$tables = array_map(function ($t) { return isClass($t) ? getClassName($t) : $t; }, $tables);
sort($tables);
return "NM_" . implode("_", $tables);
}

View File

@@ -2,8 +2,11 @@
namespace Core\Objects\DatabaseEntity;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntityHandler;
use Core\Objects\DatabaseEntity\Controller\NMRelation;
class Group extends DatabaseEntity {
@@ -33,4 +36,11 @@ class Group extends DatabaseEntity {
"color" => $this->color
];
}
public function getMembers(SQL $sql): array {
$nmTable = NMRelation::buildTableName(User::class, Group::class);
return User::findBy(User::createBuilder($sql, false)
->innerJoin($nmTable, "user_id", "User.id")
->whereEq("group_id", $this->id));
}
}

View File

@@ -227,7 +227,7 @@ function getClassPath($class, string $suffix = ".class"): string {
if ($pathCount >= 3) {
if (strcasecmp($pathParts[$pathCount - 3], "API") === 0) {
$group = $pathParts[$pathCount - 2];
if (strcasecmp($group, "Parameter") !== 0) {
if (strcasecmp($group, "Parameter") !== 0 && strcasecmp($group, "Traits") !== 0) {
$pathParts = array_slice($pathParts, 0, $pathCount - 2);
$pathParts[] = "${group}API";
}