web-base/core/Objects/DatabaseEntity/DatabaseEntityHandler.php
2022-06-17 23:29:24 +02:00

256 lines
8.1 KiB
PHP

<?php
namespace Objects\DatabaseEntity;
use Driver\Logger\Logger;
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\Query\CreateTable;
use Driver\SQL\SQL;
use Driver\SQL\Strategy\CascadeStrategy;
use Driver\SQL\Strategy\SetNullStrategy;
use PHPUnit\Util\Exception;
class DatabaseEntityHandler {
private \ReflectionClass $entityClass;
private static \ReflectionProperty $ID_FIELD;
private string $tableName;
private array $columns;
private array $properties;
private array $relations;
private SQL $sql;
private Logger $logger;
public function __construct(SQL $sql, \ReflectionClass $entityClass) {
$this->sql = $sql;
$className = $entityClass->getName();
$this->logger = new Logger($entityClass->getShortName(), $sql);
$this->entityClass = $entityClass;
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class) ||
!$this->entityClass->isInstantiable()) {
$this->raiseError("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable.");
}
$this->tableName = $this->entityClass->getShortName();
$this->columns = [];
$this->properties = [];
$this->relations = [];
if (!isset(self::$ID_FIELD)) {
self::$ID_FIELD = (new \ReflectionClass(DatabaseEntity::class))->getProperty("id");
}
foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName();
$propertyType = $property->getType();
$columnName = self::getColumnName($propertyName);
if (!($propertyType instanceof \ReflectionNamedType)) {
$this->raiseError("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($this->sql, $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) {
$this->raiseError("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;
}
public function getLogger(): Logger {
return $this->logger;
}
public function getTableName(): string {
return $this->tableName;
}
private function entityFromRow(array $row): DatabaseEntity {
try {
$entity = $this->entityClass->newInstanceWithoutConstructor();
foreach ($this->columns as $propertyName => $column) {
$value = $row[$column->getName()];
$property = $this->properties[$propertyName];
if ($property->getType()->getName() === "DateTime") {
$value = new \DateTime($value);
}
$property->setValue($entity, $value);
}
self::$ID_FIELD->setAccessible(true);
self::$ID_FIELD->setValue($entity, $row["id"]);
return $entity;
} catch (\Exception $exception) {
$this->logger->error("Error creating entity from database row: " . $exception->getMessage());
throw $exception;
}
}
public function fetchOne(int $id): ?DatabaseEntity {
$res = $this->sql->select("id", ...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(?Condition $condition = null): ?array {
$query = $this->sql->select("id", ...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 getTableQuery(): CreateTable {
$query = $this->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;
}
public function createTable(): bool {
$query = $this->getTableQuery();
return $query->execute();
}
public function insertOrUpdate(DatabaseEntity $entity) {
$id = $entity->getId();
if ($id === null) {
$columns = [];
$row = [];
foreach ($this->columns as $propertyName => $column) {
$columns[] = $column->getName();
$property = $this->properties[$propertyName];
if ($property->isInitialized($entity)) {
$value = $property->getValue($entity);
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
} else {
$this->logger->error("Cannot insert entity: property '$propertyName' was not initialized yet.");
return false;
}
$row[] = $value;
}
$res = $this->sql->insert($this->tableName, $columns)
->addRow(...$row)
->returning("id")
->execute();
if ($res !== false) {
return $this->sql->getLastInsertId();
} else {
return false;
}
} else {
$query = $this->sql->update($this->tableName)
->where(new Compare("id", $id));
foreach ($this->columns as $propertyName => $column) {
$columnName = $column->getName();
$property = $this->properties[$propertyName];
if ($property->isInitialized($entity)) {
$value = $property->getValue($entity);
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
} else {
$this->logger->error("Cannot update entity: property '$propertyName' was not initialized yet.");
return false;
}
$query->set($columnName, $value);
}
return $query->execute();
}
}
public function delete(int $id) {
return $this->sql->delete($this->tableName)->where(new Compare("id", $id))->execute();
}
private function raiseError(string $message) {
$this->logger->error($message);
throw new Exception($message);
}
private function getPropertyValue() {
}
}