web-base/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php

949 lines
34 KiB
PHP
Raw Permalink Normal View History

2022-06-17 20:53:35 +02:00
<?php
2022-11-20 17:13:53 +01:00
namespace Core\Objects\DatabaseEntity\Controller;
2022-11-18 18:06:46 +01:00
use Core\Driver\Logger\Logger;
2023-01-10 22:12:05 +01:00
use Core\Driver\SQL\Column\BigIntColumn;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Column\BoolColumn;
2022-11-20 17:13:53 +01:00
use Core\Driver\SQL\Column\Column;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Column\DateTimeColumn;
use Core\Driver\SQL\Column\EnumColumn;
use Core\Driver\SQL\Column\IntColumn;
use Core\Driver\SQL\Column\JsonColumn;
use Core\Driver\SQL\Column\StringColumn;
2022-11-20 17:13:53 +01:00
use Core\Driver\SQL\Condition\CondIn;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Column\DoubleColumn;
use Core\Driver\SQL\Column\FloatColumn;
2022-11-20 17:13:53 +01:00
use Core\Driver\SQL\Condition\CondNot;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Constraint\ForeignKey;
use Core\Driver\SQL\Join\InnerJoin;
2022-11-20 17:13:53 +01:00
use Core\Driver\SQL\Query\CreateProcedure;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Query\CreateTable;
2022-11-20 17:13:53 +01:00
use Core\Driver\SQL\Query\Insert;
2022-11-18 18:06:46 +01:00
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\SQL;
use Core\Driver\SQL\Strategy\CascadeStrategy;
use Core\Driver\SQL\Strategy\SetNullStrategy;
2022-11-20 17:13:53 +01:00
use Core\Driver\SQL\Strategy\UpdateStrategy;
use Core\Driver\SQL\Type\CurrentColumn;
use Core\Driver\SQL\Type\CurrentTable;
2023-01-10 22:12:05 +01:00
use Core\Objects\DatabaseEntity\Attribute\BigInt;
2022-11-18 18:06:46 +01:00
use Core\Objects\DatabaseEntity\Attribute\Enum;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
2022-11-27 12:33:27 +01:00
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
2022-11-18 18:06:46 +01:00
use Core\Objects\DatabaseEntity\Attribute\Json;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
2022-11-20 17:13:53 +01:00
use Core\Objects\DatabaseEntity\Attribute\Multiple;
2023-01-10 22:12:05 +01:00
use Core\Objects\DatabaseEntity\Attribute\MultipleReference;
2023-01-22 12:44:03 +01:00
use Core\Objects\DatabaseEntity\Attribute\NoFetch;
2022-11-18 18:06:46 +01:00
use Core\Objects\DatabaseEntity\Attribute\Transient;
use Core\Objects\DatabaseEntity\Attribute\Unique;
2023-03-05 15:30:06 +01:00
use Core\Objects\DatabaseEntity\Attribute\UsePropertiesOf;
2022-06-17 20:53:35 +02:00
2022-11-20 17:13:53 +01:00
class DatabaseEntityHandler implements Persistable {
2022-06-17 20:53:35 +02:00
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;
2022-11-20 17:13:53 +01:00
private array $nmRelations;
2022-11-27 12:33:27 +01:00
private array $extendingClasses;
private ?\ReflectionProperty $extendingProperty;
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();
2023-01-10 22:12:05 +01:00
$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
2022-11-20 17:13:53 +01:00
$this->relations = []; // property name => DatabaseEntityHandler
2022-06-20 19:52:31 +02:00
$this->constraints = []; // \Driver\SQL\Constraint\Constraint
2022-11-27 12:33:27 +01:00
$this->nmRelations = []; // table name => NMRelation
$this->extendingClasses = []; // enum value => \ReflectionClass
$this->extendingProperty = null; // only one attribute can hold the type of the extending class
2023-01-10 22:12:05 +01:00
}
public function init() {
2023-01-11 15:28:47 +01:00
$className = $this->entityClass->getName();
2023-03-05 15:30:06 +01:00
$entityClass = $this->entityClass;
while ($usePropsOf = self::getAttribute($entityClass, UsePropertiesOf::class)) {
$entityClass = new \ReflectionClass($usePropsOf->getClass());
}
2023-01-11 15:28:47 +01:00
2023-03-05 15:30:06 +01:00
$uniqueColumns = self::getAttribute($entityClass, Unique::class);
2023-01-10 22:12:05 +01:00
if ($uniqueColumns) {
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($uniqueColumns->getColumns());
}
2022-06-17 23:29:24 +02:00
2023-03-05 15:30:06 +01:00
foreach ($entityClass->getProperties() as $property) {
2022-06-17 20:53:35 +02:00
$propertyName = $property->getName();
2022-06-20 19:52:31 +02:00
if ($propertyName === "id") {
$this->properties[$propertyName] = $property;
continue;
}
2022-11-20 17:13:53 +01:00
if ($property->isStatic()) {
continue;
}
2022-06-17 20:53:35 +02:00
$propertyType = $property->getType();
2023-01-07 15:34:05 +01:00
$columnName = self::buildColumnName($propertyName);
2022-06-17 20:53:35 +02:00
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;
}
2022-11-27 12:33:27 +01:00
$ext = self::getAttribute($property, ExtendingEnum::class);
if ($ext !== null) {
if ($this->extendingProperty !== null) {
$this->raiseError("Cannot have more than one extending property");
} else {
$this->extendingProperty = $property;
$enumMappings = $ext->getMappings();
foreach ($enumMappings as $key => $extendingClass) {
if (!is_string($key)) {
$type = gettype($key);
$this->raiseError("Extending enum must be an array of string => class, got type '$type' for key: " . print_r($key, true));
} else if (!is_string($extendingClass)) {
$type = gettype($extendingClass);
$this->raiseError("Extending enum must be an array of string => class, got type '$type' for value: " . print_r($extendingClass, true));
}
try {
$requestedClass = new \ReflectionClass($extendingClass);
2023-03-05 15:30:06 +01:00
if (!$requestedClass->isSubclassOf($entityClass)) {
$this->raiseError("Class '$extendingClass' must be an inheriting from '" . $entityClass->getName() . "' for an extending enum");
2022-11-27 12:33:27 +01:00
} else {
$this->extendingClasses[$key] = $requestedClass;
}
} catch (\ReflectionException $ex) {
$this->raiseError("Cannot persist extending enum for class $extendingClass: " . $ex->getMessage());
}
}
}
}
2022-06-20 19:52:31 +02:00
$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 {
2023-01-10 22:12:05 +01:00
$bigInt = self::getAttribute($property, BigInt::class);
if ($bigInt) {
$this->columns[$propertyName] = new BigIntColumn($columnName, $nullable, $defaultValue, $bigInt->isUnsigned());
} else {
$maxLength = self::getAttribute($property, MaxLength::class);
$this->columns[$propertyName] = new StringColumn($columnName, $maxLength?->getValue(), $nullable, $defaultValue);
}
2022-06-20 19:52:31 +02:00
}
2022-06-17 20:53:35 +02:00
} else if ($propertyTypeName === 'int') {
$bigInt = self::getAttribute($property, BigInt::class);
if ($bigInt) {
$this->columns[$propertyName] = new BigIntColumn($columnName, $nullable, $defaultValue, $bigInt->isUnsigned());
} else {
$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);
2022-11-20 17:13:53 +01:00
} else if ($propertyTypeName === "array") {
2023-01-09 20:27:01 +01:00
$json = self::getAttribute($property, Json::class);
if ($json) {
$this->columns[$propertyName] = new JsonColumn($columnName, $nullable, $defaultValue);
} else {
2022-11-20 17:13:53 +01:00
2023-01-09 20:27:01 +01:00
$multiple = self::getAttribute($property, Multiple::class);
2023-01-10 22:12:05 +01:00
$multipleReference = self::getAttribute($property, MultipleReference::class);
if (!$multiple && !$multipleReference) {
2023-01-09 20:27:01 +01:00
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName. " .
2023-01-10 22:12:05 +01:00
"Is the 'Multiple' or 'MultipleReference' attribute missing?");
2023-01-09 20:27:01 +01:00
}
2022-11-20 17:13:53 +01:00
2023-01-09 20:27:01 +01:00
try {
2023-01-10 22:12:05 +01:00
$refClass = $multiple ? $multiple->getClassName() : $multipleReference->getClassName();
2023-01-09 20:27:01 +01:00
$requestedClass = new \ReflectionClass($refClass);
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
2023-01-10 22:12:05 +01:00
$otherHandler = DatabaseEntity::getHandler($this->sql, $requestedClass);
if ($multiple) {
$nmRelation = new NMRelation($this, $property, $otherHandler);
$this->nmRelations[$propertyName] = $nmRelation;
} else {
$thisProperty = $multipleReference->getThisProperty();
$relProperty = $multipleReference->getRelProperty();
$nmRelationReference = new NMRelationReference($otherHandler, $thisProperty, $relProperty);
$this->nmRelations[$propertyName] = $nmRelationReference;
2023-01-09 20:27:01 +01:00
}
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' of type multiple can " .
"only reference DatabaseEntity types, but got: $refClass");
}
} catch (\Exception $ex) {
$this->raiseError("Cannot persist class '$className' property '$propertyTypeName': " . $ex->getMessage());
2022-06-20 19:52:31 +02:00
}
2022-11-20 17:13:53 +01:00
}
2022-06-20 19:52:31 +02:00
} 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) {
2022-11-18 18:06:46 +01:00
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($columnName);
2022-06-20 19:52:31 +02:00
}
2022-06-17 20:53:35 +02:00
}
}
2023-01-10 22:12:05 +01:00
public function getNMRelations(): array {
return $this->nmRelations;
}
public static function getAttribute(\ReflectionProperty|\ReflectionClass $property, string $attributeClass): ?object {
2022-06-20 19:52:31 +02:00
$attributes = $property->getAttributes($attributeClass);
$attribute = array_shift($attributes);
return $attribute?->newInstance();
}
2023-01-07 15:34:05 +01:00
public static function buildColumnName(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 getColumnName(string $property, bool $withTableName = true): string {
if ($withTableName) {
if ($property === "id") {
return "$this->tableName.id";
} else {
return $this->tableName . "." . $this->columns[$property]->getName();
}
2023-01-07 15:34:05 +01:00
} else {
if ($property === "id") {
return "id";
} else {
return $this->columns[$property]->getName();
}
2023-01-07 15:34:05 +01:00
}
}
2022-06-20 19:52:31 +02:00
public function getColumnNames(): array {
$columns = ["$this->tableName.id"];
2023-01-07 15:34:05 +01:00
foreach (array_keys($this->columns) as $property) {
$columns[] = $this->getColumnName($property);
2022-06-20 19:52:31 +02:00
}
return $columns;
}
public function getColumns(): array {
return $this->columns;
}
public function dependsOn(): array {
2023-01-10 22:12:05 +01:00
$foreignTables = array_filter(array_map(
function (DatabaseEntityHandler $relationHandler) {
return $relationHandler->getTableName();
}, $this->relations),
function ($tableName) {
return $tableName !== $this->getTableName();
});
2022-06-20 19:52:31 +02:00
return array_unique($foreignTables);
}
2023-01-10 22:12:05 +01:00
public function getNMRelation(string $property): Persistable {
return $this->nmRelations[$property];
}
public function getProperty(string $property): \ReflectionProperty {
return $this->properties[$property];
}
2023-01-15 00:32:17 +01:00
public function hasProperty(string $propertyName): bool {
return isset($this->properties[$propertyName]);
}
2022-06-20 19:52:31 +02:00
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;
}
2023-01-22 12:32:18 +01:00
private function getValueFromRow(array $row, string $propertyName, mixed &$value,
int $fetchEntities = DatabaseEntityQuery::FETCH_NONE,
?DatabaseEntityQueryContext $context = null): bool {
2022-11-27 12:33:27 +01:00
$column = $this->columns[$propertyName] ?? null;
if (!$column) {
return false;
}
$columnName = $column->getName();
if (!array_key_exists($columnName, $row)) {
return false;
}
$value = $row[$columnName];
if ($column instanceof DateTimeColumn) {
if ($value !== null) {
$value = new \DateTime($value);
}
2022-11-27 12:33:27 +01:00
} else if ($column instanceof JsonColumn) {
2023-01-15 00:32:17 +01:00
$value = json_decode($value, true);
2022-11-27 12:33:27 +01:00
} else if (isset($this->relations[$propertyName])) {
2023-01-22 12:44:03 +01:00
$property = $this->properties[$propertyName];
2023-01-15 00:32:17 +01:00
$relationHandler = $this->relations[$propertyName];
2023-01-07 15:34:05 +01:00
$relColumnPrefix = self::buildColumnName($propertyName) . "_";
2022-11-27 12:33:27 +01:00
if (array_key_exists($relColumnPrefix . "id", $row)) {
$relId = $row[$relColumnPrefix . "id"];
if ($relId !== null) {
2023-01-22 12:44:03 +01:00
if ($fetchEntities !== DatabaseEntityQuery::FETCH_NONE && !self::getAttribute($property, NoFetch::class)) {
2023-01-15 00:32:17 +01:00
if ($this === $relationHandler) {
2023-01-22 12:32:18 +01:00
if ($context) {
$value = $context->queryCache($this, $relId);
if ($value) {
return true;
}
}
$subQuery = DatabaseEntityQuery::fetchOne($this)
2023-01-15 00:32:17 +01:00
->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE)
2023-01-22 12:32:18 +01:00
->whereEq($this->getColumnName("id"), $relId);
if ($context) {
$subQuery->withContext($context);
}
$value = $subQuery->execute();
if ($value !== null && $context !== null) {
$context->addCache($this, $value);
}
2023-01-15 00:32:17 +01:00
} else {
2023-01-22 12:32:18 +01:00
$prefixedRow = self::getPrefixedRow($row, $relColumnPrefix);
if ($context) {
$value = $context->queryCache($relationHandler, $prefixedRow["id"]);
if ($value) {
return true;
}
}
$subFetchMode = $fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE ? $fetchEntities : DatabaseEntityQuery::FETCH_NONE;
$value = $relationHandler->entityFromRow($prefixedRow, [], $subFetchMode, $context);
if ($context && $value) {
$context->addCache($relationHandler, $value);
}
2023-01-15 00:32:17 +01:00
}
} else {
return false;
}
2022-11-27 12:33:27 +01:00
} else if (!$column->notNull()) {
$value = null;
} else {
return false;
}
} else {
return false;
}
}
2023-01-15 00:32:17 +01:00
if ($value === null) {
$defaultValue = self::getAttribute($this->properties[$propertyName], DefaultValue::class);
if ($defaultValue) {
$value = $defaultValue->getValue();
}
}
2022-11-27 12:33:27 +01:00
return true;
}
2023-01-22 12:32:18 +01:00
public function entityFromRow(array $row, array $additionalColumns = [],
int $fetchEntities = DatabaseEntityQuery::FETCH_NONE,
?DatabaseEntityQueryContext $context = null): ?DatabaseEntity {
try {
2022-06-20 19:52:31 +02:00
2022-11-27 12:33:27 +01:00
$constructorClass = $this->entityClass;
if ($this->extendingProperty !== null) {
if ($this->getValueFromRow($row, $this->extendingProperty->getName(), $enumValue)) {
if ($enumValue && isset($this->extendingClasses[$enumValue])) {
$constructorClass = $this->extendingClasses[$enumValue];
}
}
}
2023-01-22 12:32:18 +01:00
$entity = call_user_func($constructorClass->getName() . "::newInstance", $constructorClass, $row);
2022-06-20 19:52:31 +02:00
if (!($entity instanceof DatabaseEntity)) {
$this->logger->error("Created Object is not of type DatabaseEntity");
return null;
}
2022-11-27 12:33:27 +01:00
foreach ($this->properties as $property) {
2023-01-15 00:32:17 +01:00
$propertyName = $property->getName();
2023-01-22 12:32:18 +01:00
if ($this->getValueFromRow($row, $propertyName, $value, $fetchEntities, $context)) {
2022-06-20 19:52:31 +02:00
$property->setValue($entity, $value);
2022-06-17 23:29:24 +02:00
}
}
2022-06-17 23:29:24 +02:00
foreach ($additionalColumns as $column) {
if (!in_array($column, $this->columns) && !isset($this->properties[$column])) {
$entity[$column] = $row[$column];
}
}
2022-11-20 17:13:53 +01:00
// init n:m / 1:n properties with empty arrays
2023-01-10 22:12:05 +01:00
foreach ($this->nmRelations as $propertyName => $nmRelation) {
$property = $this->properties[$propertyName];
$property->setValue($entity, []);
2022-11-20 17:13:53 +01:00
}
2022-06-20 19:52:31 +02:00
$this->properties["id"]->setValue($entity, $row["id"]);
2023-01-15 00:32:17 +01:00
2022-06-20 19:52:31 +02:00
$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
}
}
2023-01-09 14:21:11 +01:00
public function updateNM(DatabaseEntity $entity, ?array $properties = null): bool {
2022-11-20 17:13:53 +01:00
if (empty($this->nmRelations)) {
return true;
}
2023-01-10 22:12:05 +01:00
foreach ($this->nmRelations as $nmProperty => $nmRelation) {
$property = $this->properties[$nmProperty];
$nmTable = $nmRelation->getTableName();
if ($nmRelation instanceof NMRelation) {
$thisIdColumn = $nmRelation->getIdColumn($this);
$otherHandler = $nmRelation->getOtherHandler($this);
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
} else if ($nmRelation instanceof NMRelationReference) {
$otherHandler = $nmRelation->getRelHandler();
$thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false);
$refIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false);
2023-01-10 22:12:05 +01:00
} else {
throw new \Exception("updateNM not implemented for type: " . get_class($nmRelation));
}
2022-11-20 17:13:53 +01:00
// delete from n:m table if no longer exists
$deleteStatement = $this->sql->delete($nmTable)
2023-01-09 14:21:11 +01:00
->whereEq($thisIdColumn, $entity->getId()); // this condition is important
2022-11-20 17:13:53 +01:00
2023-01-10 22:12:05 +01:00
if ($properties === null || in_array($nmProperty, $properties)) {
$entityIds = array_keys($property->getValue($entity));
if (!empty($entityIds)) {
$deleteStatement->where(
new CondNot(new CondIn(new Column($refIdColumn), $entityIds))
);
2022-11-20 17:13:53 +01:00
}
2023-01-09 14:21:11 +01:00
$deleteStatement->execute();
}
2022-11-20 17:13:53 +01:00
}
2023-01-09 14:21:11 +01:00
return $this->insertNM($entity, true, $properties);
2022-11-20 17:13:53 +01:00
}
2023-01-09 14:21:11 +01:00
public function insertNM(DatabaseEntity $entity, bool $ignoreExisting = true, ?array $properties = null): bool {
2022-11-20 17:13:53 +01:00
if (empty($this->nmRelations)) {
return true;
}
$success = true;
2023-01-10 22:12:05 +01:00
foreach ($this->nmRelations as $nmProperty => $nmRelation) {
2022-11-20 17:13:53 +01:00
2023-01-10 22:12:05 +01:00
if ($properties !== null && !in_array($nmProperty, $properties)) {
continue;
2022-11-20 17:13:53 +01:00
}
2023-01-10 22:12:05 +01:00
if ($nmRelation instanceof NMRelation) {
$otherHandler = $nmRelation->getOtherHandler($this);
$thisIdColumn = $nmRelation->getIdColumn($this);
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
} else if ($nmRelation instanceof NMRelationReference) {
$otherHandler = $nmRelation->getRelHandler();
$thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false);
$refIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false);
2023-01-10 22:12:05 +01:00
} else {
throw new \Exception("insertNM not implemented for type: " . get_class($nmRelation));
}
2023-01-09 14:21:11 +01:00
2023-01-10 22:12:05 +01:00
$property = $this->properties[$nmProperty];
$relEntities = $property->getValue($entity);
if (!empty($relEntities)) {
if ($nmRelation instanceof NMRelation) {
$columns = [$thisIdColumn, $refIdColumn];
$nmTable = $nmRelation->getTableName();
$statement = $this->sql->insert($nmTable, $columns);
if ($ignoreExisting) {
$statement->onDuplicateKeyStrategy(new UpdateStrategy($columns, [
$thisIdColumn => $entity->getId()
]));
}
foreach ($relEntities as $relEntity) {
$relEntityId = (is_int($relEntity) ? $relEntity : $relEntity->getId());
$statement->addRow($entity->getId(), $relEntityId);
}
$success = $statement->execute() && $success;
} else if ($nmRelation instanceof NMRelationReference) {
$otherHandler = $nmRelation->getRelHandler();
$thisIdProperty = $otherHandler->properties[$nmRelation->getThisProperty()];
foreach ($relEntities as $relEntity) {
$thisIdProperty->setValue($relEntity, $entity);
2022-11-20 17:13:53 +01:00
}
2023-01-16 21:47:23 +01:00
$statement = $otherHandler->getInsertQuery($relEntities);
if ($ignoreExisting) {
$columns = $nmRelation->getRefColumns();
$statement->onDuplicateKeyStrategy(new UpdateStrategy($columns, [
$thisIdColumn => $entity->getId()
]));
}
$success = $statement->execute() && $success;
2023-01-10 22:12:05 +01:00
}
2023-01-09 14:21:11 +01:00
}
2022-11-20 17:13:53 +01:00
}
return $success;
}
2023-01-22 12:32:18 +01:00
public function fetchNMRelations(array $entities, int $fetchEntities = DatabaseEntityQuery::FETCH_DIRECT,
?DatabaseEntityQueryContext $context = null) {
2022-11-20 17:13:53 +01:00
2023-01-15 00:32:17 +01:00
if ($fetchEntities === DatabaseEntityQuery::FETCH_NONE) {
return;
}
2023-01-22 12:32:18 +01:00
$entityIds = array_keys($entities);
2022-11-20 17:13:53 +01:00
2023-01-22 12:32:18 +01:00
// N:M
if (!empty($this->nmRelations) && !empty($entityIds)) {
2022-11-20 17:13:53 +01:00
2023-01-22 12:32:18 +01:00
foreach ($this->nmRelations as $nmProperty => $nmRelation) {
$nmTable = $nmRelation->getTableName();
$property = $this->properties[$nmProperty];
2023-01-22 12:44:03 +01:00
if (self::getAttribute($property, NoFetch::class)) {
continue;
}
2023-01-16 21:47:23 +01:00
2023-01-22 12:32:18 +01:00
if ($nmRelation instanceof NMRelation) {
$thisIdColumn = $nmRelation->getIdColumn($this);
$otherHandler = $nmRelation->getOtherHandler($this);
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
$refTableName = $otherHandler->getTableName();
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id"))
->addSelectValue(new Column($thisIdColumn))
->where(new CondIn(new Column($thisIdColumn), $entityIds));
$relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE);
$rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) {
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
return;
}
2023-01-10 22:12:05 +01:00
2023-01-22 12:32:18 +01:00
$relEntities = [];
foreach ($rows as $row) {
$relEntity = $context?->queryCache($otherHandler, $row["id"]);
if (!$relEntity) {
$relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities, $context);
$context?->addCache($otherHandler, $relEntity);
$relEntities[$relEntity->getId()] = $relEntity;
}
2022-11-20 17:13:53 +01:00
2023-01-22 12:32:18 +01:00
$thisEntity = $entities[$row[$thisIdColumn]];
$targetArray = $property->getValue($thisEntity);
$targetArray[$relEntity->getId()] = $relEntity;
$property->setValue($thisEntity, $targetArray);
2023-01-10 22:12:05 +01:00
}
2022-11-20 17:13:53 +01:00
2023-01-22 12:32:18 +01:00
if ($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE) {
$otherHandler->fetchNMRelations($relEntities, $fetchEntities, $context);
}
} else if ($nmRelation instanceof NMRelationReference) {
2022-11-20 17:13:53 +01:00
2023-01-22 12:32:18 +01:00
$otherHandler = $nmRelation->getRelHandler();
$thisIdColumn = $otherHandler->getColumnName($nmRelation->getThisProperty(), false);
$relIdColumn = $otherHandler->getColumnName($nmRelation->getRefProperty(), false);
2023-01-15 00:32:17 +01:00
2023-01-22 12:32:18 +01:00
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
->where(new CondIn(new Column($thisIdColumn), $entityIds));
$relEntityQuery->fetchEntities($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE);
$rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) {
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
return;
}
$thisIdProperty = $otherHandler->properties[$nmRelation->getThisProperty()];
$relEntities = [];
foreach ($rows as $row) {
$relEntity = $context?->queryCache($otherHandler, $row["id"]);
if (!$relEntity) {
$relEntity = $otherHandler->entityFromRow($row, [], $fetchEntities, $context);
$context?->addCache($otherHandler, $relEntity);
$relEntities[$relEntity->getId()] = $relEntity;
}
2022-11-20 17:13:53 +01:00
2023-01-22 12:32:18 +01:00
$thisEntity = $entities[$row[$thisIdColumn]];
$thisIdProperty->setValue($relEntity, $thisEntity);
$targetArray = $property->getValue($thisEntity);
$targetArray[$row[$relIdColumn]] = $relEntity;
$property->setValue($thisEntity, $targetArray);
}
2023-01-16 21:47:23 +01:00
2023-01-22 12:32:18 +01:00
if ($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE) {
$otherHandler->fetchNMRelations($relEntities, $fetchEntities, $context);
}
} else {
$this->logger->error("fetchNMRelations for type '" . get_class($nmRelation) . "' is not implemented");
2022-11-20 17:13:53 +01:00
}
2023-01-11 14:05:45 +01:00
}
2023-01-22 12:32:18 +01:00
}
2023-01-11 14:05:45 +01:00
2023-01-22 12:32:18 +01:00
// if fetch mode is recursive, fetch all relations of our fetched relations as well...
if ($fetchEntities === DatabaseEntityQuery::FETCH_RECURSIVE) {
foreach ($entities as $entity) {
foreach ($this->relations as $propertyName => $relHandler) {
$property = $this->properties[$propertyName];
if ($property->isInitialized($entity)) {
$relEntity = $property->getValue($entity);
if ($relEntity) {
$relEntityId = $relEntity->getId(); // $fetchedEntities
$relTableName = $relHandler->getTableName();
if (!isset($fetchedEntities[$relTableName]) || !isset($fetchedEntities[$relTableName][$relEntityId])) {
$relHandler->fetchNMRelations([$relEntityId => $relEntity], DatabaseEntityQuery::FETCH_RECURSIVE, $context);
}
}
}
2023-01-11 14:05:45 +01:00
}
2022-11-20 17:13:53 +01: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()
->whereEq($this->tableName . ".id", $id)
2022-06-17 20:53:35 +02:00
->first()
->execute();
2022-11-20 17:13:53 +01:00
if ($res !== false && $res !== null) {
$res = $this->entityFromRow($res);
2022-06-17 20:53:35 +02:00
}
2022-11-20 17:13:53 +01:00
return $res;
2022-06-17 20:53:35 +02:00
}
public function getCreateQueries(SQL $sql, bool $canExist = false): array {
2022-11-20 17:13:53 +01:00
$queries = [];
$table = $this->getTableName();
// Create Table
$queries[] = $this->getTableQuery($sql, $canExist);
2022-11-20 17:13:53 +01:00
// pre defined values
$getPredefinedValues = $this->entityClass->getMethod("getPredefinedValues");
2023-01-09 20:27:01 +01:00
$predefinedValues = $getPredefinedValues->invoke(null);
if ($predefinedValues) {
$queries[] = $this->getInsertQuery($predefinedValues);
}
// Entity Log
2022-11-20 17:13:53 +01:00
$entityLogConfig = $this->entityClass->getProperty("entityLogConfig");
$entityLogConfig = $entityLogConfig->getValue();
if (isset($entityLogConfig["insert"]) && $entityLogConfig["insert"] === true) {
$trigger = $sql->createTrigger("${table}_trg_insert")
2022-11-20 17:13:53 +01:00
->after()->insert($table)
->exec(new CreateProcedure($sql, "InsertEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("id"),
"lifetime" => $entityLogConfig["lifetime"] ?? 90,
]);
if ($canExist) {
$trigger->onlyIfNotExist();
}
$queries[] = $trigger;
2022-11-20 17:13:53 +01:00
}
if (isset($entityLogConfig["update"]) && $entityLogConfig["update"] === true) {
$trigger = $sql->createTrigger("${table}_trg_update")
2022-11-20 17:13:53 +01:00
->after()->update($table)
->exec(new CreateProcedure($sql, "UpdateEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("id"),
]);
if ($canExist) {
$trigger->onlyIfNotExist();
}
$queries[] = $trigger;
2022-11-20 17:13:53 +01:00
}
if (isset($entityLogConfig["delete"]) && $entityLogConfig["delete"] === true) {
$trigger = $sql->createTrigger("${table}_trg_delete")
2022-11-20 17:13:53 +01:00
->after()->delete($table)
->exec(new CreateProcedure($sql, "DeleteEntityLog"), [
"tableName" => new CurrentTable(),
"entityId" => new CurrentColumn("id"),
]);
if ($canExist) {
$trigger->onlyIfNotExist();
}
$queries[] = $trigger;
2022-11-20 17:13:53 +01:00
}
return $queries;
}
public function getTableQuery(SQL $sql, bool $canExist = false): CreateTable {
2022-11-20 17:13:53 +01:00
$query = $sql->createTable($this->tableName)
2022-06-17 20:53:35 +02:00
->addSerial("id")
->primaryKey("id");
if ($canExist) {
$query->onlyIfNotExists();
}
2022-06-17 20:53:35 +02:00
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;
}
2023-01-09 14:21:11 +01:00
private function prepareRow(DatabaseEntity $entity, string $action, ?array $properties = null): bool|array {
2022-06-20 19:52:31 +02:00
$row = [];
2024-05-04 16:40:37 +02:00
if ($entity->getId() !== null) {
$row["id"] = $entity->getId();
}
2022-06-20 19:52:31 +02:00
foreach ($this->columns as $propertyName => $column) {
2023-01-09 14:21:11 +01:00
if ($properties !== null && !in_array($propertyName, $properties)) {
2022-06-20 19:52:31 +02:00
continue;
}
$property = $this->properties[$propertyName];
if ($property->isInitialized($entity)) {
$value = $property->getValue($entity);
if (isset($this->relations[$propertyName])) {
2022-11-19 01:15:34 +01:00
$value = $value?->getId();
2022-06-20 19:52:31 +02:00
}
} 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;
}
2023-01-09 14:21:11 +01:00
public function update(DatabaseEntity $entity, ?array $properties = null, bool $saveNM = false) {
$row = $this->prepareRow($entity, "update", $properties);
2022-08-20 22:17:17 +02:00
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)
->whereEq($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
2023-01-09 14:21:11 +01:00
$res = empty($row) ? true : $query->execute();
2022-11-20 17:13:53 +01:00
if ($res && $saveNM) {
2023-01-09 14:21:11 +01:00
$res = $this->updateNM($entity, $properties);
2022-11-20 17:13:53 +01:00
}
2023-01-12 20:55:32 +01:00
$entity->postUpdate();
2022-11-20 17:13:53 +01:00
return $res;
2022-08-20 22:17:17 +02:00
}
2022-06-17 20:53:35 +02:00
2022-11-20 17:13:53 +01:00
public function insert(DatabaseEntity $entity): bool|int {
2022-08-20 22:17:17 +02:00
$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;
}
}
2023-01-09 14:21:11 +01:00
public function insertOrUpdate(DatabaseEntity $entity, ?array $properties = null, bool $saveNM = false) {
2022-08-20 22:17:17 +02:00
$id = $entity->getId();
if ($id === null) {
return $this->insert($entity);
} else {
2023-01-09 14:21:11 +01:00
return $this->update($entity, $properties, $saveNM);
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)
->whereEq($this->tableName . ".id", $id)
2022-06-20 19:52:31 +02:00
->execute();
}
private function raiseError(string $message) {
$this->logger->error($message);
2022-11-27 12:33:27 +01:00
throw new \Exception($message);
2022-06-17 20:53:35 +02:00
}
2022-11-19 01:15:34 +01:00
public function getSQL(): SQL {
return $this->sql;
}
2022-11-20 17:13:53 +01:00
public function getInsertQuery(DatabaseEntity|array $entities): ?Insert {
if (empty($entities)) {
return null;
}
$firstEntity = (is_array($entities) ? current($entities) : $entities);
$firstRow = $this->prepareRow($firstEntity, "insert");
2022-11-27 12:33:27 +01:00
if ($firstRow === false) {
return null;
}
2022-11-20 17:13:53 +01:00
$statement = $this->sql->insert($this->tableName, array_keys($firstRow))
->addRow(...array_values($firstRow));
if (is_array($entities)) {
foreach ($entities as $entity) {
if ($entity === $firstEntity) {
continue;
}
$row = $this->prepareRow($entity, "insert");
$statement->addRow(...array_values($row));
}
}
return $statement;
}
2022-06-17 20:53:35 +02:00
}