DB Entities, SQL Update

This commit is contained in:
Roman 2022-06-17 20:53:35 +02:00
parent 6bbf517196
commit 6d600d4004
17 changed files with 443 additions and 70 deletions

@ -95,8 +95,6 @@ namespace Api\Contact {
if (!$insertDB) {
$message .= " Mail: $dbError";
}
error_log($message);
}
if (!$sendMail && !$insertDB) {

@ -182,13 +182,14 @@ namespace Api\Mail {
$this->success = @$mail->Send();
if (!$this->success) {
$this->lastError = "Error sending Mail: $mail->ErrorInfo";
error_log("sendMail() failed: $mail->ErrorInfo");
$this->logger->error("sendMail() failed: $mail->ErrorInfo");
} else {
$this->result["messageId"] = $mail->getLastMessageID();
}
} catch (Exception $e) {
$this->success = false;
$this->lastError = "Error sending Mail: $e";
$this->logger->error($this->lastError);
}
return $this->success;

@ -6,6 +6,12 @@ use Driver\Logger\Logger;
use Objects\User;
use PhpMqtt\Client\MqttClient;
/**
* TODO: we need following features, probably as abstract/generic class/method:
* - easy way for pagination (select with limit/offset)
* - CRUD Endpoints/Objects (Create, Update, Delete)
*/
abstract class Request {
protected User $user;

@ -35,7 +35,7 @@ class Stats extends Request {
return ($this->success ? $res[0]["count"] : 0);
}
private function checkSettings() {
private function checkSettings(): bool {
$req = new \Api\Settings\Get($this->user);
$this->success = $req->execute(array("key" => "^(mail_enabled|recaptcha_enabled)$"));
$this->lastError = $req->getLastError();

@ -51,8 +51,7 @@ class VerifyCaptcha extends Request {
$score = $response["score"];
if ($action !== $response["action"]) {
$this->createError("Could not verify captcha: Action does not match");
}
else if($score < 0.7) {
} else if ($score < 0.7) {
$this->createError("Could not verify captcha: Google ReCaptcha Score < 0.7 (Your score: $score), you are likely a bot");
}
}

@ -8,9 +8,6 @@ use \Driver\SQL\Strategy\CascadeStrategy;
class CreateDatabase extends DatabaseScript {
// NOTE:
// explicit serial ids removed due to postgres' serial implementation
public static function createQueries(SQL $sql): array {
$queries = array();

@ -1,8 +1,6 @@
<?php
namespace Driver\SQL\Query;
use Driver\SQL\Column\IntColumn;
namespace Driver\SQL\Column;
class BigIntColumn extends IntColumn {

@ -0,0 +1,10 @@
<?php
namespace Driver\SQL\Column;
class DoubleColumn extends NumericColumn {
public function __construct(string $name, bool $nullable, $defaultValue = NULL, ?int $totalDigits = null, ?int $decimalDigits = null) {
parent::__construct($name, $nullable, $defaultValue, $totalDigits, $decimalDigits);
$this->type = "DOUBLE";
}
}

@ -0,0 +1,10 @@
<?php
namespace Driver\SQL\Column;
class FloatColumn extends NumericColumn {
public function __construct(string $name, bool $nullable, $defaultValue = NULL, ?int $totalDigits = null, ?int $decimalDigits = null) {
parent::__construct($name, $nullable, $defaultValue, $totalDigits, $decimalDigits);
$this->type = "FLOAT";
}
}

@ -0,0 +1,31 @@
<?php
namespace Driver\SQL\Column;
use Driver\SQL\Column\Column;
class NumericColumn extends Column {
protected string $type;
private ?int $totalDigits;
private ?int $decimalDigits;
public function __construct(string $name, bool $nullable, $defaultValue = NULL, ?int $totalDigits = null, ?int $decimalDigits = null) {
parent::__construct($name, $nullable, $defaultValue);
$this->totalDigits = $totalDigits;
$this->decimalDigits = $decimalDigits;
$this->type = "NUMERIC";
}
public function getDecimalDigits(): ?int {
return $this->decimalDigits;
}
public function getTotalDigits(): ?int {
return $this->totalDigits;
}
public function getTypeName(): string {
return $this->type;
}
}

@ -7,6 +7,7 @@ use \Api\Parameter\Parameter;
use DateTime;
use \Driver\SQL\Column\Column;
use \Driver\SQL\Column\IntColumn;
use Driver\SQL\Column\NumericColumn;
use \Driver\SQL\Column\SerialColumn;
use \Driver\SQL\Column\StringColumn;
use \Driver\SQL\Column\EnumColumn;
@ -267,6 +268,19 @@ class MySQL extends SQL {
return "BOOLEAN";
} else if ($column instanceof JsonColumn) {
return "LONGTEXT"; # some maria db setups don't allow JSON here…
} else if ($column instanceof NumericColumn) {
$digitsTotal = $column->getTotalDigits();
$digitsDecimal = $column->getDecimalDigits();
$type = $column->getTypeName();
if ($digitsTotal !== null) {
if ($digitsDecimal !== null) {
return "$type($digitsTotal,$digitsDecimal)";
} else {
return "$type($digitsTotal)";
}
} else {
return $type;
}
} else {
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
return NULL;

@ -6,6 +6,7 @@ use \Api\Parameter\Parameter;
use Driver\SQL\Column\Column;
use \Driver\SQL\Column\IntColumn;
use Driver\SQL\Column\NumericColumn;
use \Driver\SQL\Column\SerialColumn;
use \Driver\SQL\Column\StringColumn;
use \Driver\SQL\Column\EnumColumn;
@ -244,6 +245,17 @@ class PostgreSQL extends SQL {
return "BOOLEAN";
} else if ($column instanceof JsonColumn) {
return "JSON";
} else if ($column instanceof NumericColumn) {
$digitsDecimal = $column->getDecimalDigits();
$type = $column->getTypeName();
if ($digitsDecimal !== null) {
if ($type === "double") {
$type = "float"; // postgres doesn't know about double :/
}
return "$type($digitsDecimal)";
} else {
return $type;
}
} else {
$this->lastError = $this->logger->error("Unsupported Column Type: " . get_class($column));
return NULL;
@ -329,7 +341,9 @@ class PostgreSQL extends SQL {
if ($col instanceof KeyWord) {
return $col->getValue();
} elseif (is_array($col)) {
$columns = array_map(function ($c) { return $this->columnName($c); }, $col);
$columns = array_map(function ($c) {
return $this->columnName($c);
}, $col);
return implode(",", $columns);
} else {
if (($index = strrpos($col, ".")) !== FALSE) {

@ -2,6 +2,11 @@
namespace Driver\SQL\Query;
use Driver\SQL\Column\BigIntColumn;
use Driver\SQL\Column\Column;
use Driver\SQL\Column\DoubleColumn;
use Driver\SQL\Column\FloatColumn;
use Driver\SQL\Column\NumericColumn;
use Driver\SQL\Column\SerialColumn;
use Driver\SQL\Column\StringColumn;
use Driver\SQL\Column\IntColumn;
@ -10,6 +15,7 @@ use Driver\SQL\Column\EnumColumn;
use Driver\SQL\Column\BoolColumn;
use Driver\SQL\Column\JsonColumn;
use Driver\SQL\Constraint\Constraint;
use Driver\SQL\Constraint\PrimaryKey;
use Driver\SQL\Constraint\Unique;
use Driver\SQL\Constraint\ForeignKey;
@ -31,6 +37,16 @@ class CreateTable extends Query {
$this->ifNotExists = false;
}
public function addColumn(Column $column): CreateTable {
$this->columns[$column->getName()] = $column;
return $this;
}
public function addConstraint(Constraint $constraint): CreateTable {
$this->constraints[] = $constraint;
return $this;
}
public function addSerial(string $name): CreateTable {
$this->columns[$name] = new SerialColumn($name);
return $this;
@ -71,6 +87,21 @@ class CreateTable extends Query {
return $this;
}
public function addNumeric(string $name, bool $nullable = false, $defaultValue = NULL, ?int $digitsTotal = 10, ?int $digitsDecimal = 0): CreateTable {
$this->columns[$name] = new NumericColumn($name, $nullable, $defaultValue, $digitsTotal, $digitsDecimal);
return $this;
}
public function addFloat(string $name, bool $nullable = false, $defaultValue = NULL, ?int $digitsTotal = null, ?int $digitsDecimal = null): CreateTable {
$this->columns[$name] = new FloatColumn($name, $nullable, $defaultValue, $digitsTotal, $digitsDecimal);
return $this;
}
public function addDouble(string $name, bool $nullable = false, $defaultValue = NULL, ?int $digitsTotal = null, ?int $digitsDecimal = null): CreateTable {
$this->columns[$name] = new DoubleColumn($name, $nullable, $defaultValue, $digitsTotal, $digitsDecimal);
return $this;
}
public function primaryKey(...$names): CreateTable {
$this->constraints[] = new PrimaryKey($names);
return $this;

@ -0,0 +1,75 @@
<?php
namespace Objects\DatabaseEntity;
use Driver\Logger\Logger;
use Driver\SQL\Condition\Condition;
use Driver\SQL\SQL;
abstract class DatabaseEntity {
private static array $handlers = [];
private ?int $id;
public function __construct() {
$this->id = null;
}
public static function find(SQL $sql, int $id): ?DatabaseEntity {
$handler = self::getHandler();
return $handler->fetchOne($sql, $id);
}
public static function findAll(SQL $sql, ?Condition $condition): ?array {
$handler = self::getHandler();
return $handler->fetchMultiple($sql, $condition);
}
public function save(SQL $sql): bool {
$handler = self::getHandler();
$res = $handler->insertOrUpdate($sql, $this);
if ($res === false) {
return false;
} else if ($this->id === null) {
$this->id = $res;
}
return true;
}
public function delete(SQL $sql): bool {
$handler = self::getHandler();
if ($this->id === null) {
$className = $handler->getReflection()->getName();
(new Logger("DatabaseEntity", $sql))->error("Cannot delete entity of class '$className' without id");
return false;
}
return $handler->delete($sql, $this->id);
}
public static function getHandler($obj_or_class = null): DatabaseEntityHandler {
if (!$obj_or_class) {
$obj_or_class = get_called_class();
}
if (!($obj_or_class instanceof \ReflectionClass)) {
$class = new \ReflectionClass($obj_or_class);
} else {
$class = $obj_or_class;
}
$handler = self::$handlers[$class->getShortName()] ?? null;
if (!$handler) {
$handler = new DatabaseEntityHandler($class);
self::$handlers[$class->getShortName()] = $handler;
}
return $handler;
}
public function getId(): ?int {
return $this->id;
}
}

@ -0,0 +1,188 @@
<?php
namespace Objects\DatabaseEntity;
use Driver\SQL\Column\BoolColumn;
use Driver\SQL\Column\DateTimeColumn;
use Driver\SQL\Column\IntColumn;
use Driver\SQL\Column\StringColumn;
use Driver\SQL\Condition\Compare;
use Driver\SQL\Condition\Condition;
use Driver\SQL\Column\DoubleColumn;
use Driver\SQL\Column\FloatColumn;
use Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\SQL;
use Driver\SQL\Strategy\CascadeStrategy;
use Driver\SQL\Strategy\SetNullStrategy;
use PHPUnit\Util\Exception;
class DatabaseEntityHandler {
private \ReflectionClass $entityClass;
private string $tableName;
private array $columns;
private array $properties;
public function __construct(\ReflectionClass $entityClass) {
$className = $entityClass->getName();
$this->entityClass = $entityClass;
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class) ||
!$this->entityClass->isInstantiable()) {
throw new Exception("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable.");
}
$this->tableName = $this->entityClass->getShortName();
$this->columns = [];
$this->properties = [];
$this->relations = [];
foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName();
$propertyType = $property->getType();
$columnName = self::getColumnName($propertyName);
if (!($propertyType instanceof \ReflectionNamedType)) {
throw new Exception("Cannot persist class '$className': Property '$propertyName' has no valid type");
}
$nullable = $propertyType->allowsNull();
$propertyTypeName = $propertyType->getName();
if ($propertyTypeName === 'string') {
$this->columns[$propertyName] = new StringColumn($columnName, null, $nullable);
} else if ($propertyTypeName === 'int') {
$this->columns[$propertyName] = new IntColumn($columnName, $nullable);
} else if ($propertyTypeName === 'float') {
$this->columns[$propertyName] = new FloatColumn($columnName, $nullable);
} else if ($propertyTypeName === 'double') {
$this->columns[$propertyName] = new DoubleColumn($columnName, $nullable);
} else if ($propertyTypeName === 'bool') {
$this->columns[$propertyName] = new BoolColumn($columnName, $nullable);
} else if ($propertyTypeName === 'DateTime') {
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable);
} else {
try {
$requestedClass = new \ReflectionClass($propertyTypeName);
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
$requestedHandler = ($requestedClass->getName() === $this->entityClass->getName()) ?
$this : DatabaseEntity::getHandler($requestedClass);
$strategy = $nullable ? new SetNullStrategy() : new CascadeStrategy();
$this->columns[$propertyName] = new IntColumn($columnName, $nullable);
$this->relations[$propertyName] = new ForeignKey($columnName, $requestedHandler->tableName, "id", $strategy);
}
} catch (\Exception $ex) {
throw new Exception("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
}
}
$this->properties[$propertyName] = $property;
}
}
private static function getColumnName(string $propertyName): string {
// abcTestLOL => abc_test_lol
return strtolower(preg_replace_callback("/([a-z])([A-Z]+)/", function ($m) {
return $m[1] . "_" . strtolower($m[2]);
}, $propertyName));
}
public function getReflection(): \ReflectionClass {
return $this->entityClass;
}
private function entityFromRow(array $row): DatabaseEntity {
$entity = $this->entityClass->newInstanceWithoutConstructor();
foreach ($this->columns as $propertyName => $column) {
$this->properties[$propertyName]->setValue($entity, $row[$column]);
}
return $entity;
}
public function fetchOne(SQL $sql, int $id): ?DatabaseEntity {
$res = $sql->select(...array_keys($this->columns))
->from($this->tableName)
->where(new Compare("id", $id))
->first()
->execute();
if (empty($res)) {
return null;
} else {
return $this->entityFromRow($res);
}
}
public function fetchMultiple(SQL $sql, ?Condition $condition = null): ?array {
$query = $sql->select(...array_keys($this->columns))
->from($this->tableName);
if ($condition) {
$query->where($condition);
}
$res = $query->execute();
if ($res === false) {
return null;
} else {
$entities = [];
foreach ($res as $row) {
$entities[] = $this->entityFromRow($row);
}
return $entities;
}
}
public function createTable(SQL $sql): bool {
$query = $sql->createTable($this->tableName)
->onlyIfNotExists()
->addSerial("id")
->primaryKey("id");
foreach ($this->columns as $column) {
$query->addColumn($column);
}
foreach ($this->relations as $constraint) {
$query->addConstraint($constraint);
}
return $query->execute();
}
public function insertOrUpdate(SQL $sql, DatabaseEntity $entity) {
$id = $entity->getId();
if ($id === null) {
$columns = [];
$row = [];
foreach ($this->columns as $propertyName => $column) {
$columns[] = $column->getName();
$row[] = $this->properties[$propertyName]->getValue($entity);
}
$res = $sql->insert($this->tableName, $columns)
->addRow(...$row)
->returning("id")
->execute();
if ($res !== false) {
return $sql->getLastInsertId();
} else {
return false;
}
} else {
$query = $sql->update($this->tableName)
->where(new Compare("id", $id));
foreach ($this->columns as $propertyName => $column) {
$columnName = $column->getName();
$value = $this->properties[$propertyName]->getValue($entity);
$query->set($columnName, $value);
}
return $query->execute();
}
}
public function delete(SQL $sql, int $id) {
return $sql->delete($this->tableName)->where(new Compare("id", $id))->execute();
}
}

@ -1,5 +1,10 @@
<?php
/**
* This file contains functions used globally without a namespace and should not require
* any other files. It also loads the composer vendor libraries.
*/
$autoLoad = implode(DIRECTORY_SEPARATOR, [__DIR__, "External", "vendor", "autoload.php"]);
if (is_file($autoLoad)) {
require_once $autoLoad;
@ -186,10 +191,6 @@ function intendCode($code, $escape = true): string {
return $newCode;
}
function replaceCssSelector($sel) {
return preg_replace("~[.#<>]~", "_", preg_replace("~[:\-]~", "", $sel));
}
function html_attributes(array $attributes): string {
return implode(" ", array_map(function ($key) use ($attributes) {
$value = htmlspecialchars($attributes[$key]);