web-base/core/Objects/DatabaseEntity/DatabaseEntityHandler.php

415 lines
14 KiB
PHP
Raw Normal View History

2022-06-17 20:53:35 +02:00
<?php
namespace Objects\DatabaseEntity;
use Driver\Logger\Logger;
2022-06-17 20:53:35 +02:00
use Driver\SQL\Column\BoolColumn;
use Driver\SQL\Column\DateTimeColumn;
2022-06-20 19:52:31 +02:00
use Driver\SQL\Column\EnumColumn;
2022-06-17 20:53:35 +02:00
use Driver\SQL\Column\IntColumn;
2022-06-20 19:52:31 +02:00
use Driver\SQL\Column\JsonColumn;
2022-06-17 20:53:35 +02:00
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;
2022-06-20 19:52:31 +02:00
use Driver\SQL\Query\Select;
2022-06-17 20:53:35 +02:00
use Driver\SQL\SQL;
use Driver\SQL\Strategy\CascadeStrategy;
use Driver\SQL\Strategy\SetNullStrategy;
2022-06-20 19:52:31 +02:00
use Objects\DatabaseEntity\Attribute\Enum;
use Objects\DatabaseEntity\Attribute\DefaultValue;
use Objects\DatabaseEntity\Attribute\Json;
use Objects\DatabaseEntity\Attribute\Many;
use Objects\DatabaseEntity\Attribute\MaxLength;
use Objects\DatabaseEntity\Attribute\Transient;
use Objects\DatabaseEntity\Attribute\Unique;
2022-06-17 20:53:35 +02:00
use PHPUnit\Util\Exception;
class DatabaseEntityHandler {
private \ReflectionClass $entityClass;
private string $tableName;
private array $columns;
private array $properties;
2022-06-17 23:29:24 +02:00
private array $relations;
2022-06-20 19:52:31 +02:00
private array $constraints;
private SQL $sql;
private Logger $logger;
2022-06-17 20:53:35 +02:00
public function __construct(SQL $sql, \ReflectionClass $entityClass) {
$this->sql = $sql;
2022-06-17 20:53:35 +02:00
$className = $entityClass->getName();
$this->logger = new Logger($entityClass->getShortName(), $sql);
2022-06-17 20:53:35 +02:00
$this->entityClass = $entityClass;
2022-06-20 19:52:31 +02:00
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class)) {
$this->raiseError("Cannot persist class '$className': Not an instance of DatabaseEntity or not instantiable.");
2022-06-17 20:53:35 +02:00
}
$this->tableName = $this->entityClass->getShortName();
2022-06-20 19:52:31 +02:00
$this->columns = []; // property name => database column name
$this->properties = []; // property name => \ReflectionProperty
$this->relations = []; // property name => referenced table name
$this->constraints = []; // \Driver\SQL\Constraint\Constraint
2022-06-17 23:29:24 +02:00
2022-06-17 20:53:35 +02:00
foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName();
2022-06-20 19:52:31 +02:00
if ($propertyName === "id") {
$this->properties[$propertyName] = $property;
continue;
}
2022-06-17 20:53:35 +02:00
$propertyType = $property->getType();
$columnName = self::getColumnName($propertyName);
if (!($propertyType instanceof \ReflectionNamedType)) {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has no valid type");
2022-06-17 20:53:35 +02:00
}
$nullable = $propertyType->allowsNull();
$propertyTypeName = $propertyType->getName();
2022-06-20 19:52:31 +02:00
if (!empty($property->getAttributes(Transient::class))) {
continue;
}
$defaultValue = (self::getAttribute($property, DefaultValue::class))?->getValue();
$isUnique = !empty($property->getAttributes(Unique::class));
2022-06-17 20:53:35 +02:00
if ($propertyTypeName === 'string') {
2022-06-20 19:52:31 +02:00
$enum = self::getAttribute($property, Enum::class);
if ($enum) {
$this->columns[$propertyName] = new EnumColumn($columnName, $enum->getValues(), $nullable, $defaultValue);
} else {
$maxLength = self::getAttribute($property, MaxLength::class);
$this->columns[$propertyName] = new StringColumn($columnName, $maxLength?->getValue(), $nullable, $defaultValue);
}
2022-06-17 20:53:35 +02:00
} else if ($propertyTypeName === 'int') {
2022-06-20 19:52:31 +02:00
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
2022-06-17 20:53:35 +02:00
} else if ($propertyTypeName === 'float') {
2022-06-20 19:52:31 +02:00
$this->columns[$propertyName] = new FloatColumn($columnName, $nullable, $defaultValue);
2022-06-17 20:53:35 +02:00
} else if ($propertyTypeName === 'double') {
2022-06-20 19:52:31 +02:00
$this->columns[$propertyName] = new DoubleColumn($columnName, $nullable, $defaultValue);
2022-06-17 20:53:35 +02:00
} else if ($propertyTypeName === 'bool') {
2022-06-20 19:52:31 +02:00
$this->columns[$propertyName] = new BoolColumn($columnName, $defaultValue ?? false);
2022-06-17 20:53:35 +02:00
} else if ($propertyTypeName === 'DateTime') {
2022-06-20 19:52:31 +02:00
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable, $defaultValue);
/*} else if ($propertyName === 'array') {
$many = self::getAttribute($property, Many::class);
if ($many) {
$requestedType = $many->getValue();
if (isClass($requestedType)) {
$requestedClass = new \ReflectionClass($requestedType);
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $requestedType");
}
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
}*/
} else if ($propertyTypeName !== "mixed") {
2022-06-17 20:53:35 +02:00
try {
$requestedClass = new \ReflectionClass($propertyTypeName);
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
2022-06-20 19:52:31 +02:00
$columnName .= "_id";
2022-06-17 20:53:35 +02:00
$requestedHandler = ($requestedClass->getName() === $this->entityClass->getName()) ?
$this : DatabaseEntity::getHandler($this->sql, $requestedClass);
2022-06-17 20:53:35 +02:00
$strategy = $nullable ? new SetNullStrategy() : new CascadeStrategy();
2022-06-20 19:52:31 +02:00
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
$this->constraints[] = new ForeignKey($columnName, $requestedHandler->tableName, "id", $strategy);
$this->relations[$propertyName] = $requestedHandler;
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
2022-06-17 20:53:35 +02:00
}
} catch (\Exception $ex) {
2022-06-20 19:52:31 +02:00
$this->raiseError("Cannot persist class '$className' property '$propertyTypeName': " . $ex->getMessage());
}
} else {
if (!empty($property->getAttributes(Json::class))) {
$this->columns[$propertyName] = new JsonColumn($columnName, $nullable, $defaultValue);
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
2022-06-17 20:53:35 +02:00
}
}
$this->properties[$propertyName] = $property;
2022-06-20 19:52:31 +02:00
if ($isUnique) {
$this->constraints[] = new \Driver\SQL\Constraint\Unique($columnName);
}
2022-06-17 20:53:35 +02:00
}
}
2022-06-20 19:52:31 +02:00
private static function getAttribute(\ReflectionProperty $property, string $attributeClass): ?object {
$attributes = $property->getAttributes($attributeClass);
$attribute = array_shift($attributes);
return $attribute?->newInstance();
}
public static function getColumnName(string $propertyName): string {
2022-06-17 20:53:35 +02:00
// 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;
}
2022-06-17 23:29:24 +02:00
public function getTableName(): string {
return $this->tableName;
}
2022-06-20 19:52:31 +02:00
public function getRelations(): array {
return $this->relations;
}
public function getColumnNames(): array {
$columns = ["$this->tableName.id"];
foreach ($this->columns as $column) {
$columns[] = $this->tableName . "." . $column->getName();
}
return $columns;
}
public function getColumns(): array {
return $this->columns;
}
public function dependsOn(): array {
$foreignTables = array_map(function (DatabaseEntityHandler $relationHandler) {
return $relationHandler->getTableName();
}, $this->relations);
return array_unique($foreignTables);
}
public static function getPrefixedRow(array $row, string $prefix): array {
$rel_row = [];
foreach ($row as $relKey => $relValue) {
if (startsWith($relKey, $prefix)) {
$rel_row[substr($relKey, strlen($prefix))] = $relValue;
}
}
return $rel_row;
}
public function entityFromRow(array $row): ?DatabaseEntity {
try {
2022-06-20 19:52:31 +02:00
$entity = call_user_func($this->entityClass->getName() . "::newInstance", $this->entityClass, $row);
if (!($entity instanceof DatabaseEntity)) {
$this->logger->error("Created Object is not of type DatabaseEntity");
return null;
}
foreach ($this->columns as $propertyName => $column) {
2022-06-20 19:52:31 +02:00
$columnName = $column->getName();
if (array_key_exists($columnName, $row)) {
$value = $row[$columnName];
$property = $this->properties[$propertyName];
if ($column instanceof DateTimeColumn) {
$value = new \DateTime($value);
} else if ($column instanceof JsonColumn) {
$value = json_decode($value);
} else if (isset($this->relations[$propertyName])) {
$relColumnPrefix = self::getColumnName($propertyName) . "_";
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));
} else if (!$column->notNull()) {
$value = null;
} else {
continue;
}
} else {
continue;
}
}
2022-06-17 23:29:24 +02:00
2022-06-20 19:52:31 +02:00
$property->setAccessible(true);
$property->setValue($entity, $value);
2022-06-17 23:29:24 +02:00
}
}
2022-06-17 23:29:24 +02:00
2022-06-20 19:52:31 +02:00
$this->properties["id"]->setAccessible(true);
$this->properties["id"]->setValue($entity, $row["id"]);
$entity->postFetch($this->sql, $row);
return $entity;
} catch (\Exception $exception) {
$this->logger->error("Error creating entity from database row: " . $exception->getMessage());
2022-06-20 19:52:31 +02:00
return null;
2022-06-17 20:53:35 +02:00
}
}
2022-06-20 19:52:31 +02:00
public function getSelectQuery(): Select {
return $this->sql->select(...$this->getColumnNames())
->from($this->tableName);
}
public function fetchOne(int $id): DatabaseEntity|bool|null {
$res = $this->getSelectQuery()
->where(new Compare($this->tableName . ".id", $id))
2022-06-17 20:53:35 +02:00
->first()
->execute();
2022-06-20 19:52:31 +02:00
if ($res === false || $res === null) {
return $res;
2022-06-17 20:53:35 +02:00
} else {
return $this->entityFromRow($res);
}
}
public function fetchMultiple(?Condition $condition = null): ?array {
2022-06-20 19:52:31 +02:00
$query = $this->getSelectQuery();
2022-06-17 20:53:35 +02:00
if ($condition) {
$query->where($condition);
}
$res = $query->execute();
if ($res === false) {
return null;
} else {
$entities = [];
foreach ($res as $row) {
2022-06-20 19:52:31 +02:00
$entity = $this->entityFromRow($row);
if ($entity) {
$entities[$entity->getId()] = $entity;
}
2022-06-17 20:53:35 +02:00
}
return $entities;
}
}
public function getTableQuery(): CreateTable {
$query = $this->sql->createTable($this->tableName)
2022-06-17 20:53:35 +02:00
->onlyIfNotExists()
->addSerial("id")
->primaryKey("id");
foreach ($this->columns as $column) {
$query->addColumn($column);
}
2022-06-20 19:52:31 +02:00
foreach ($this->constraints as $constraint) {
2022-06-17 20:53:35 +02:00
$query->addConstraint($constraint);
}
return $query;
}
public function createTable(): bool {
$query = $this->getTableQuery();
2022-06-17 20:53:35 +02:00
return $query->execute();
}
2022-08-20 22:17:17 +02:00
private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null) {
2022-06-20 19:52:31 +02:00
$row = [];
foreach ($this->columns as $propertyName => $column) {
if ($columns && !in_array($column->getName(), $columns)) {
continue;
}
$property = $this->properties[$propertyName];
$property->setAccessible(true);
if ($property->isInitialized($entity)) {
$value = $property->getValue($entity);
if (isset($this->relations[$propertyName])) {
$value = $value->getId();
}
} else if (!$this->columns[$propertyName]->notNull()) {
$value = null;
} else {
2022-08-20 22:17:17 +02:00
$defaultValue = self::getAttribute($property, DefaultValue::class);
if ($defaultValue) {
$value = $defaultValue->getValue();
} else if ($action !== "update") {
2022-06-20 19:52:31 +02:00
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
2022-06-17 23:29:24 +02:00
return false;
2022-06-20 19:52:31 +02:00
} else {
continue;
2022-06-17 23:29:24 +02:00
}
2022-06-17 20:53:35 +02:00
}
2022-06-20 19:52:31 +02:00
$row[$column->getName()] = $value;
}
2022-08-20 22:17:17 +02:00
return $row;
}
public function update(DatabaseEntity $entity, ?array $columns = null) {
$row = $this->prepareRow($entity, "update", $columns);
if ($row === false) {
return false;
}
2022-06-20 19:52:31 +02:00
$entity->preInsert($row);
2022-08-20 22:17:17 +02:00
$query = $this->sql->update($this->tableName)
->where(new Compare($this->tableName . ".id", $entity->getId()));
2022-06-20 19:52:31 +02:00
2022-08-20 22:17:17 +02:00
foreach ($row as $columnName => $value) {
$query->set($columnName, $value);
}
2022-06-20 19:52:31 +02:00
2022-08-20 22:17:17 +02:00
return $query->execute();
}
2022-06-17 20:53:35 +02:00
2022-08-20 22:17:17 +02:00
public function insert(DatabaseEntity $entity) {
$row = $this->prepareRow($entity, "insert");
if ($row === false) {
return false;
}
2022-06-17 23:29:24 +02:00
2022-08-20 22:17:17 +02:00
$entity->preInsert($row);
// insert with id?
$entityId = $entity->getId();
if ($entityId !== null) {
$row["id"] = $entityId;
}
$query = $this->sql->insert($this->tableName, array_keys($row))
->addRow(...array_values($row));
2022-06-17 20:53:35 +02:00
2022-08-20 22:17:17 +02:00
// return id if its auto-generated
if ($entityId === null) {
$query->returning("id");
}
$res = $query->execute();
if ($res !== false) {
return $this->sql->getLastInsertId();
} else {
return false;
}
}
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
$id = $entity->getId();
if ($id === null) {
return $this->insert($entity);
} else {
return $this->update($entity, $columns);
2022-06-17 20:53:35 +02:00
}
}
public function delete(int $id) {
2022-06-20 19:52:31 +02:00
return $this->sql
->delete($this->tableName)
->where(new Compare($this->tableName . ".id", $id))
->execute();
}
private function raiseError(string $message) {
$this->logger->error($message);
throw new Exception($message);
2022-06-17 20:53:35 +02:00
}
}