NMRelation cleanup / improvement

This commit is contained in:
Roman 2023-01-10 22:12:05 +01:00
parent f14a7a4762
commit 13f7866d42
13 changed files with 303 additions and 272 deletions

@ -13,6 +13,7 @@ namespace Core\API\Database {
use Core\API\DatabaseAPI; use Core\API\DatabaseAPI;
use Core\API\Parameter\StringType; use Core\API\Parameter\StringType;
use Core\Objects\Context; use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
class Status extends DatabaseAPI { class Status extends DatabaseAPI {
@ -48,6 +49,10 @@ namespace Core\API\Database {
$classPath = "\\$baseDir\\Objects\\DatabaseEntity\\$className"; $classPath = "\\$baseDir\\Objects\\DatabaseEntity\\$className";
if (isClass($classPath)) { if (isClass($classPath)) {
$class = new \ReflectionClass($classPath); $class = new \ReflectionClass($classPath);
if (!$class->isSubclassOf(DatabaseEntity::class)) {
$class = null;
continue;
}
break; break;
} }
} }

@ -62,8 +62,9 @@ namespace Core\API\Groups {
return false; return false;
} }
$nmTable = User::getHandler($sql)->getNMRelation("groups")->getTableName();
$memberCount = new Alias($sql->select(new Count()) $memberCount = new Alias($sql->select(new Count())
->from(NMRelation::buildTableName("User", "Group")) ->from($nmTable)
->whereEq("group_id", new Column("Group.id")), "memberCount"); ->whereEq("group_id", new Column("Group.id")), "memberCount");
$groupsQuery = $this->createPaginationQuery($sql, [$memberCount]); $groupsQuery = $this->createPaginationQuery($sql, [$memberCount]);
@ -119,7 +120,7 @@ namespace Core\API\Groups {
protected function _execute(): bool { protected function _execute(): bool {
$sql = $this->context->getSQL(); $sql = $this->context->getSQL();
$nmTable = NMRelation::buildTableName(User::class, Group::class); $nmTable = User::getHandler($sql)->getNMRelation("groups")->getTableName();
$condition = new Compare("group_id", $this->getParam("id")); $condition = new Compare("group_id", $this->getParam("id"));
$nmJoin = new InnerJoin($nmTable, "$nmTable.user_id", "User.id"); $nmJoin = new InnerJoin($nmTable, "$nmTable.user_id", "User.id");
if (!$this->initPagination($sql, User::class, $condition, 100, [$nmJoin])) { if (!$this->initPagination($sql, User::class, $condition, 100, [$nmJoin])) {

@ -30,7 +30,7 @@ class CondIn extends Condition {
$values = implode(",", $values); $values = implode(",", $values);
$values = "($values)"; $values = "($values)";
} else if($haystack instanceof Select) { } else if($haystack instanceof Select) {
$values = $haystack->build($params); $values = $haystack->getExpression($sql, $params);
} else { } else {
$sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack)); $sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack));
return false; return false;

@ -0,0 +1,16 @@
<?php
namespace Core\Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)] class BigInt {
private bool $unsigned;
public function __construct(bool $unsigned = false) {
$this->unsigned = $unsigned;
}
public function isUnsigned(): bool {
return $this->unsigned;
}
}

@ -14,5 +14,4 @@ class Multiple {
public function getClassName(): string { public function getClassName(): string {
return $this->className; return $this->className;
} }
} }

@ -0,0 +1,32 @@
<?php
namespace Core\Objects\DatabaseEntity\Attribute;
namespace Core\Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class MultipleReference {
private string $className;
private string $thisProperty;
private string $relProperty;
public function __construct(string $className, string $thisProperty, string $relProperty) {
$this->className = $className;
$this->thisProperty = $thisProperty;
$this->relProperty = $relProperty;
}
public function getClassName(): string {
return $this->className;
}
public function getThisProperty(): string {
return $this->thisProperty;
}
public function getRelProperty(): string {
return $this->relProperty;
}
}

@ -2,6 +2,15 @@
namespace Core\Objects\DatabaseEntity\Attribute; namespace Core\Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)] class Unique { #[\Attribute(\Attribute::TARGET_PROPERTY|\Attribute::TARGET_CLASS)] class Unique {
private array $columns;
public function __construct(string ...$columns) {
$this->columns = $columns;
}
public function getColumns(): array {
return $this->columns;
}
} }

@ -226,6 +226,7 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable {
if (!$handler || $allowOverride) { if (!$handler || $allowOverride) {
$handler = new DatabaseEntityHandler($sql, $class); $handler = new DatabaseEntityHandler($sql, $class);
self::$handlers[$class->getShortName()] = $handler; self::$handlers[$class->getShortName()] = $handler;
$handler->init();
} }
return $handler; return $handler;

@ -3,6 +3,7 @@
namespace Core\Objects\DatabaseEntity\Controller; namespace Core\Objects\DatabaseEntity\Controller;
use Core\Driver\Logger\Logger; use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Column\BigIntColumn;
use Core\Driver\SQL\Column\BoolColumn; use Core\Driver\SQL\Column\BoolColumn;
use Core\Driver\SQL\Column\Column; use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Column\DateTimeColumn; use Core\Driver\SQL\Column\DateTimeColumn;
@ -10,14 +11,11 @@ use Core\Driver\SQL\Column\EnumColumn;
use Core\Driver\SQL\Column\IntColumn; use Core\Driver\SQL\Column\IntColumn;
use Core\Driver\SQL\Column\JsonColumn; use Core\Driver\SQL\Column\JsonColumn;
use Core\Driver\SQL\Column\StringColumn; use Core\Driver\SQL\Column\StringColumn;
use Core\Driver\SQL\Condition\CondAnd;
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondIn; use Core\Driver\SQL\Condition\CondIn;
use Core\Driver\SQL\Condition\Condition; use Core\Driver\SQL\Condition\Condition;
use Core\Driver\SQL\Column\DoubleColumn; use Core\Driver\SQL\Column\DoubleColumn;
use Core\Driver\SQL\Column\FloatColumn; use Core\Driver\SQL\Column\FloatColumn;
use Core\Driver\SQL\Condition\CondNot; use Core\Driver\SQL\Condition\CondNot;
use Core\Driver\SQL\Condition\CondOr;
use Core\Driver\SQL\Constraint\ForeignKey; use Core\Driver\SQL\Constraint\ForeignKey;
use Core\Driver\SQL\Join\InnerJoin; use Core\Driver\SQL\Join\InnerJoin;
use Core\Driver\SQL\Query\CreateProcedure; use Core\Driver\SQL\Query\CreateProcedure;
@ -30,12 +28,14 @@ use Core\Driver\SQL\Strategy\SetNullStrategy;
use Core\Driver\SQL\Strategy\UpdateStrategy; use Core\Driver\SQL\Strategy\UpdateStrategy;
use Core\Driver\SQL\Type\CurrentColumn; use Core\Driver\SQL\Type\CurrentColumn;
use Core\Driver\SQL\Type\CurrentTable; use Core\Driver\SQL\Type\CurrentTable;
use Core\Objects\DatabaseEntity\Attribute\BigInt;
use Core\Objects\DatabaseEntity\Attribute\Enum; use Core\Objects\DatabaseEntity\Attribute\Enum;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum; use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
use Core\Objects\DatabaseEntity\Attribute\Json; use Core\Objects\DatabaseEntity\Attribute\Json;
use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Attribute\Multiple; use Core\Objects\DatabaseEntity\Attribute\Multiple;
use Core\Objects\DatabaseEntity\Attribute\MultipleReference;
use Core\Objects\DatabaseEntity\Attribute\Transient; use Core\Objects\DatabaseEntity\Attribute\Transient;
use Core\Objects\DatabaseEntity\Attribute\Unique; use Core\Objects\DatabaseEntity\Attribute\Unique;
@ -56,6 +56,7 @@ class DatabaseEntityHandler implements Persistable {
public function __construct(SQL $sql, \ReflectionClass $entityClass) { public function __construct(SQL $sql, \ReflectionClass $entityClass) {
$this->sql = $sql; $this->sql = $sql;
$className = $entityClass->getName(); $className = $entityClass->getName();
$this->logger = new Logger($entityClass->getShortName(), $sql); $this->logger = new Logger($entityClass->getShortName(), $sql);
$this->entityClass = $entityClass; $this->entityClass = $entityClass;
if (!$this->entityClass->isSubclassOf(DatabaseEntity::class)) { if (!$this->entityClass->isSubclassOf(DatabaseEntity::class)) {
@ -70,6 +71,13 @@ class DatabaseEntityHandler implements Persistable {
$this->nmRelations = []; // table name => NMRelation $this->nmRelations = []; // table name => NMRelation
$this->extendingClasses = []; // enum value => \ReflectionClass $this->extendingClasses = []; // enum value => \ReflectionClass
$this->extendingProperty = null; // only one attribute can hold the type of the extending class $this->extendingProperty = null; // only one attribute can hold the type of the extending class
}
public function init() {
$uniqueColumns = self::getAttribute($this->entityClass, Unique::class);
if ($uniqueColumns) {
$this->constraints[] = new \Core\Driver\SQL\Constraint\Unique($uniqueColumns->getColumns());
}
foreach ($this->entityClass->getProperties() as $property) { foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName(); $propertyName = $property->getName();
@ -132,8 +140,13 @@ class DatabaseEntityHandler implements Persistable {
if ($enum) { if ($enum) {
$this->columns[$propertyName] = new EnumColumn($columnName, $enum->getValues(), $nullable, $defaultValue); $this->columns[$propertyName] = new EnumColumn($columnName, $enum->getValues(), $nullable, $defaultValue);
} else { } else {
$maxLength = self::getAttribute($property, MaxLength::class); $bigInt = self::getAttribute($property, BigInt::class);
$this->columns[$propertyName] = new StringColumn($columnName, $maxLength?->getValue(), $nullable, $defaultValue); 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);
}
} }
} else if ($propertyTypeName === 'int') { } else if ($propertyTypeName === 'int') {
$this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue); $this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue);
@ -152,25 +165,26 @@ class DatabaseEntityHandler implements Persistable {
} else { } else {
$multiple = self::getAttribute($property, Multiple::class); $multiple = self::getAttribute($property, Multiple::class);
if (!$multiple) { $multipleReference = self::getAttribute($property, MultipleReference::class);
if (!$multiple && !$multipleReference) {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName. " . $this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName. " .
"Is the 'Multiple' attribute missing?"); "Is the 'Multiple' or 'MultipleReference' attribute missing?");
} }
try { try {
$refClass = $multiple->getClassName(); $refClass = $multiple ? $multiple->getClassName() : $multipleReference->getClassName();
$requestedClass = new \ReflectionClass($refClass); $requestedClass = new \ReflectionClass($refClass);
if ($requestedClass->isSubclassOf(DatabaseEntity::class)) { if ($requestedClass->isSubclassOf(DatabaseEntity::class)) {
$nmTableName = NMRelation::buildTableName($this->getTableName(), $requestedClass->getShortName()); $otherHandler = DatabaseEntity::getHandler($this->sql, $requestedClass);
$nmRelation = $this->nmRelations[$nmTableName] ?? null; if ($multiple) {
if (!$nmRelation) { $nmRelation = new NMRelation($this, $property, $otherHandler);
$otherHandler = DatabaseEntity::getHandler($this->sql, $requestedClass); $this->nmRelations[$propertyName] = $nmRelation;
$otherNM = $otherHandler->getNMRelations(); } else {
$nmRelation = $otherNM[$nmTableName] ?? (new NMRelation($this, $otherHandler)); $thisProperty = $multipleReference->getThisProperty();
$this->nmRelations[$nmTableName] = $nmRelation; $relProperty = $multipleReference->getRelProperty();
$nmRelationReference = new NMRelationReference($otherHandler, $thisProperty, $relProperty);
$this->nmRelations[$propertyName] = $nmRelationReference;
} }
$this->nmRelations[$nmTableName]->addProperty($this, $property);
} else { } else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' of type multiple can " . $this->raiseError("Cannot persist class '$className': Property '$propertyName' of type multiple can " .
"only reference DatabaseEntity types, but got: $refClass"); "only reference DatabaseEntity types, but got: $refClass");
@ -212,7 +226,11 @@ class DatabaseEntityHandler implements Persistable {
} }
} }
public static function getAttribute(\ReflectionProperty $property, string $attributeClass): ?object { public function getNMRelations(): array {
return $this->nmRelations;
}
public static function getAttribute(\ReflectionProperty|\ReflectionClass $property, string $attributeClass): ?object {
$attributes = $property->getAttributes($attributeClass); $attributes = $property->getAttributes($attributeClass);
$attribute = array_shift($attributes); $attribute = array_shift($attributes);
return $attribute?->newInstance(); return $attribute?->newInstance();
@ -241,10 +259,6 @@ class DatabaseEntityHandler implements Persistable {
return $this->relations; return $this->relations;
} }
public function getNMRelations(): array {
return $this->nmRelations;
}
public function getColumnName(string $property): string { public function getColumnName(string $property): string {
if ($property === "id") { if ($property === "id") {
return "$this->tableName.id"; return "$this->tableName.id";
@ -267,12 +281,20 @@ class DatabaseEntityHandler implements Persistable {
} }
public function dependsOn(): array { public function dependsOn(): array {
$foreignTables = array_map(function (DatabaseEntityHandler $relationHandler) { $foreignTables = array_filter(array_map(
return $relationHandler->getTableName(); function (DatabaseEntityHandler $relationHandler) {
}, $this->relations); return $relationHandler->getTableName();
}, $this->relations),
function ($tableName) {
return $tableName !== $this->getTableName();
});
return array_unique($foreignTables); return array_unique($foreignTables);
} }
public function getNMRelation(string $property): Persistable {
return $this->nmRelations[$property];
}
public static function getPrefixedRow(array $row, string $prefix): array { public static function getPrefixedRow(array $row, string $prefix): array {
$rel_row = []; $rel_row = [];
foreach ($row as $relKey => $relValue) { foreach ($row as $relKey => $relValue) {
@ -355,11 +377,10 @@ class DatabaseEntityHandler implements Persistable {
} }
// init n:m / 1:n properties with empty arrays // init n:m / 1:n properties with empty arrays
foreach ($this->nmRelations as $nmRelation) { foreach ($this->nmRelations as $propertyName => $nmRelation) {
foreach ($nmRelation->getProperties($this) as $property) { $property = $this->properties[$propertyName];
$property->setAccessible(true); $property->setAccessible(true);
$property->setValue($entity, []); $property->setValue($entity, []);
}
} }
$this->properties["id"]->setAccessible(true); $this->properties["id"]->setAccessible(true);
@ -377,57 +398,32 @@ class DatabaseEntityHandler implements Persistable {
return true; return true;
} }
foreach ($this->nmRelations as $nmTable => $nmRelation) { foreach ($this->nmRelations as $nmProperty => $nmRelation) {
$property = $this->properties[$nmProperty];
$thisIdColumn = $nmRelation->getIdColumn($this); $nmTable = $nmRelation->getTableName();
$thisTableName = $this->getTableName();
$dataColumns = $nmRelation->getDataColumns();
$otherHandler = $nmRelation->getOtherHandler($this);
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
if ($nmRelation instanceof NMRelation) {
$thisIdColumn = $nmRelation->getIdColumn($this);
$otherHandler = $nmRelation->getOtherHandler($this);
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
} else if ($nmRelation instanceof NMRelationReference) {
$thisIdColumn = self::buildColumnName($nmRelation->getThisProperty());
$refIdColumn = self::buildColumnName($nmRelation->getRefProperty());
} else {
throw new \Exception("updateNM not implemented for type: " . get_class($nmRelation));
}
// delete from n:m table if no longer exists // delete from n:m table if no longer exists
$doDelete = true;
$deleteStatement = $this->sql->delete($nmTable) $deleteStatement = $this->sql->delete($nmTable)
->whereEq($thisIdColumn, $entity->getId()); // this condition is important ->whereEq($thisIdColumn, $entity->getId()); // this condition is important
if (!empty($dataColumns)) { if ($properties === null || in_array($nmProperty, $properties)) {
$conditions = []; $entityIds = array_keys($property->getValue($entity));
$doDelete = false; if (!empty($entityIds)) {
foreach ($dataColumns[$thisTableName] as $propertyName => $columnName) { $deleteStatement->where(
if ($properties !== null && !in_array($propertyName, $properties)) { new CondNot(new CondIn(new Column($refIdColumn), $entityIds))
continue; );
}
$property = $this->properties[$propertyName];
$entityIds = array_keys($property->getValue($entity));
if (!empty($entityIds)) {
$conditions[] = new CondAnd(
new CondBool($columnName),
new CondNot(new CondIn(new Column($refIdColumn), $entityIds)),
);
}
} }
if (!empty($conditions)) {
$deleteStatement->where(new CondOr(...$conditions));
$doDelete = true;
}
} else {
$property = next($nmRelation->getProperties($this));
if ($properties !== null && !in_array($property->getName(), $properties)) {
$doDelete = false;
} else {
$entityIds = array_keys($property->getValue($entity));
if (!empty($entityIds)) {
$deleteStatement->where(
new CondNot(new CondIn(new Column($refIdColumn), $entityIds))
);
}
}
}
if ($doDelete) {
$deleteStatement->execute(); $deleteStatement->execute();
} }
} }
@ -442,52 +438,52 @@ class DatabaseEntityHandler implements Persistable {
} }
$success = true; $success = true;
foreach ($this->nmRelations as $nmTable => $nmRelation) { foreach ($this->nmRelations as $nmProperty => $nmRelation) {
$otherHandler = $nmRelation->getOtherHandler($this);
$thisIdColumn = $nmRelation->getIdColumn($this);
$thisTableName = $this->getTableName();
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
$dataColumns = $nmRelation->getDataColumns();
$columns = [ if ($properties !== null && !in_array($nmProperty, $properties)) {
$thisIdColumn, continue;
$refIdColumn,
];
if (!empty($dataColumns)) {
$columns = array_merge($columns, array_values($dataColumns[$thisTableName]));
} }
$statement = $this->sql->insert($nmTable, $columns); if ($nmRelation instanceof NMRelation) {
if ($ignoreExisting) { $otherHandler = $nmRelation->getOtherHandler($this);
$statement->onDuplicateKeyStrategy(new UpdateStrategy($nmRelation->getAllColumns(), [ $thisIdColumn = $nmRelation->getIdColumn($this);
$thisIdColumn => $entity->getId() $refIdColumn = $nmRelation->getIdColumn($otherHandler);
])); } else if ($nmRelation instanceof NMRelationReference) {
$thisIdColumn = self::buildColumnName($nmRelation->getThisProperty());
$refIdColumn = self::buildColumnName($nmRelation->getRefProperty());
} else {
throw new \Exception("insertNM not implemented for type: " . get_class($nmRelation));
} }
$doInsert = false; $property = $this->properties[$nmProperty];
foreach ($nmRelation->getProperties($this) as $property) { $property->setAccessible(true);
if ($properties !== null && !in_array($property->getName(), $properties)) { $relEntities = $property->getValue($entity);
continue; if (!empty($relEntities)) {
} if ($nmRelation instanceof NMRelation) {
$columns = [$thisIdColumn, $refIdColumn];
$property->setAccessible(true); $nmTable = $nmRelation->getTableName();
$relEntities = $property->getValue($entity); $statement = $this->sql->insert($nmTable, $columns);
foreach ($relEntities as $relEntity) { if ($ignoreExisting) {
$relEntityId = (is_int($relEntity) ? $relEntity : $relEntity->getId()); $statement->onDuplicateKeyStrategy(new UpdateStrategy($columns, [
$nmRow = [$entity->getId(), $relEntityId]; $thisIdColumn => $entity->getId()
if (!empty($dataColumns)) { ]));
foreach (array_keys($dataColumns[$thisTableName]) as $propertyName) {
$nmRow[] = $property->getName() === $propertyName;
}
} }
$statement->addRow(...$nmRow); foreach ($relEntities as $relEntity) {
$doInsert = true; $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()];
$thisIdProperty->setAccessible(true);
if ($doInsert) { foreach ($relEntities as $relEntity) {
$success = $statement->execute() && $success; $thisIdProperty->setValue($relEntity, $entity);
}
$success = $otherHandler->getInsertQuery($relEntities)->execute() && $success;
}
} }
} }
@ -500,7 +496,7 @@ class DatabaseEntityHandler implements Persistable {
foreach ($entities as $entity) { foreach ($entities as $entity) {
foreach ($this->relations as $propertyName => $relHandler) { foreach ($this->relations as $propertyName => $relHandler) {
$property = $this->properties[$propertyName]; $property = $this->properties[$propertyName];
if ($property->isInitialized($entity) || true) { if ($property->isInitialized($entity)) {
$relEntity = $this->properties[$propertyName]->getValue($entity); $relEntity = $this->properties[$propertyName]->getValue($entity);
if ($relEntity) { if ($relEntity) {
$relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true); $relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true);
@ -515,66 +511,66 @@ class DatabaseEntityHandler implements Persistable {
} }
$entityIds = array_keys($entities); $entityIds = array_keys($entities);
foreach ($this->nmRelations as $nmTable => $nmRelation) { foreach ($this->nmRelations as $nmProperty => $nmRelation) {
$otherHandler = $nmRelation->getOtherHandler($this); $nmTable = $nmRelation->getTableName();
$property = $this->properties[$nmProperty];
$property->setAccessible(true);
$thisIdColumn = $nmRelation->getIdColumn($this); if ($nmRelation instanceof NMRelation) {
$thisProperties = $nmRelation->getProperties($this); $thisIdColumn = $nmRelation->getIdColumn($this);
$thisTableName = $this->getTableName(); $otherHandler = $nmRelation->getOtherHandler($this);
$refIdColumn = $nmRelation->getIdColumn($otherHandler);
$refTableName = $otherHandler->getTableName();
$refIdColumn = $nmRelation->getIdColumn($otherHandler); $relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
$refProperties = $nmRelation->getProperties($otherHandler); ->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id"))
$refTableName = $otherHandler->getTableName(); ->addSelectValue(new Column($thisIdColumn))
->where(new CondIn(new Column($thisIdColumn), $entityIds));
$dataColumns = $nmRelation->getDataColumns(); $rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) {
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler) $this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id")) return;
->where(new CondIn(new Column($thisIdColumn), $entityIds));
$relEntityQuery->addSelectValue(new Column($thisIdColumn));
foreach ($dataColumns as $tableDataColumns) {
foreach ($tableDataColumns as $columnName) {
$relEntityQuery->addSelectValue(new Column($columnName));
}
}
$rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) {
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
return;
}
$relEntities = [];
foreach ($rows as $row) {
$relId = $row["id"];
if (!isset($relEntities[$relId])) {
$relEntity = $otherHandler->entityFromRow($row, [], $recursive);
$relEntities[$relId] = $relEntity;
} }
$thisEntity = $entities[$row[$thisIdColumn]]; $relEntities = [];
$relEntity = $relEntities[$relId]; foreach ($rows as $row) {
$mappings = [ $relId = $row["id"];
[$refProperties, $refTableName, $relEntity, $thisEntity], if (!isset($relEntities[$relId])) {
[$thisProperties, $thisTableName, $thisEntity, $relEntity], $relEntity = $otherHandler->entityFromRow($row, [], $recursive);
]; $relEntities[$relId] = $relEntity;
foreach ($mappings as $mapping) {
list($properties, $tableName, $targetEntity, $entityToAdd) = $mapping;
foreach ($properties as $propertyName => $property) {
$addToProperty = empty($dataColumns);
if (!$addToProperty) {
$columnName = $dataColumns[$tableName][$propertyName] ?? null;
$addToProperty = ($columnName && $this->sql->parseBool($row[$columnName]));
}
if ($addToProperty) {
$targetArray = $property->getValue($targetEntity);
$targetArray[$entityToAdd->getId()] = $entityToAdd;
$property->setValue($targetEntity, $targetArray);
}
} }
$thisEntity = $entities[$row[$thisIdColumn]];
$relEntity = $relEntities[$relId];
$targetArray = $property->getValue($thisEntity);
$targetArray[$relEntity->getId()] = $relEntity;
$property->setValue($thisEntity, $targetArray);
}
} else if ($nmRelation instanceof NMRelationReference) {
$otherHandler = $nmRelation->getRelHandler();
$thisIdColumn = self::buildColumnName($nmRelation->getThisProperty());
$relIdColumn = self::buildColumnName($nmRelation->getRefProperty());
$relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler)
->where(new CondIn(new Column($thisIdColumn), $entityIds));
$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()];
$thisIdProperty->setAccessible(true);
foreach ($rows as $row) {
$relEntity = $otherHandler->entityFromRow($row, [], $recursive);
$thisEntity = $entities[$row[$thisIdColumn]];
$thisIdProperty->setValue($relEntity, $thisEntity);
$targetArray = $property->getValue($thisEntity);
$targetArray[$row[$relIdColumn]] = $relEntity;
$property->setValue($thisEntity, $targetArray);
} }
} }
} }

@ -2,135 +2,65 @@
namespace Core\Objects\DatabaseEntity\Controller; namespace Core\Objects\DatabaseEntity\Controller;
# TODO: Allow more than 2 relations here?
use Core\Driver\SQL\Query\CreateTable; use Core\Driver\SQL\Query\CreateTable;
use Core\Driver\SQL\SQL; use Core\Driver\SQL\SQL;
use Core\Driver\SQL\Strategy\CascadeStrategy; use Core\Driver\SQL\Strategy\CascadeStrategy;
class NMRelation implements Persistable { class NMRelation implements Persistable {
private DatabaseEntityHandler $handlerA; private DatabaseEntityHandler $thisHandler;
private DatabaseEntityHandler $handlerB; private DatabaseEntityHandler $otherHandler;
private array $properties; private \ReflectionProperty $property;
private string $tableName;
public function __construct(DatabaseEntityHandler $handlerA, DatabaseEntityHandler $handlerB) { public function __construct(DatabaseEntityHandler $thisHandler, \ReflectionProperty $thisProperty, DatabaseEntityHandler $otherHandler) {
$this->handlerA = $handlerA; $this->thisHandler = $thisHandler;
$this->handlerB = $handlerB; $this->otherHandler = $otherHandler;
$tableNameA = $handlerA->getTableName(); $this->property = $thisProperty;
$tableNameB = $handlerB->getTableName(); $this->tableName = "NM_" . $thisHandler->getTableName() . "_" .
if ($tableNameA === $tableNameB) { DatabaseEntityHandler::buildColumnName($thisProperty->getName());
throw new \Exception("Cannot create N:M Relation with only one table");
}
$this->properties = [
$tableNameA => [],
$tableNameB => [],
];
}
public function addProperty(DatabaseEntityHandler $src, \ReflectionProperty $property): void {
$this->properties[$src->getTableName()][$property->getName()] = $property;
} }
public function getIdColumn(DatabaseEntityHandler $handler): string { public function getIdColumn(DatabaseEntityHandler $handler): string {
return DatabaseEntityHandler::buildColumnName($handler->getTableName()) . "_id"; return DatabaseEntityHandler::buildColumnName($handler->getTableName()) . "_id";
} }
public function getDataColumns(): array { public function getProperty(): \ReflectionProperty {
return $this->property;
$referenceCount = 0;
$columnsNeeded = false;
// if in one of the relations we have multiple references, we need to differentiate
foreach ($this->properties as $refProperties) {
$referenceCount += count($refProperties);
if ($referenceCount > 1) {
$columnsNeeded = true;
break;
}
}
$columns = [];
if ($columnsNeeded) {
foreach ($this->properties as $tableName => $properties) {
$columns[$tableName] = [];
foreach ($properties as $property) {
$columnName = DatabaseEntityHandler::buildColumnName($tableName) . "_" .
DatabaseEntityHandler::buildColumnName($property->getName());
$columns[$tableName][$property->getName()] = $columnName;
}
}
}
return $columns;
}
public function getAllColumns(): array {
$relIdA = $this->getIdColumn($this->handlerA);
$relIdB = $this->getIdColumn($this->handlerB);
$columns = [$relIdA, $relIdB];
foreach ($this->getDataColumns() as $dataColumns) {
foreach ($dataColumns as $columnName) {
$columns[] = $columnName;
}
}
return $columns;
} }
public function getTableQuery(SQL $sql): CreateTable { public function getTableQuery(SQL $sql): CreateTable {
$tableNameA = $this->handlerA->getTableName(); $thisTable = $this->thisHandler->getTableName();
$tableNameB = $this->handlerB->getTableName(); $otherTable = $this->otherHandler->getTableName();
$thisIdColumn = $this->getIdColumn($this->thisHandler);
$otherIdColumn = $this->getIdColumn($this->otherHandler);
$columns = $this->getAllColumns(); return $sql->createTable($this->tableName)
list ($relIdA, $relIdB) = $columns; ->addInt($thisIdColumn)
$dataColumns = array_slice($columns, 2); ->addInt($otherIdColumn)
$query = $sql->createTable(self::buildTableName($tableNameA, $tableNameB)) ->foreignKey($thisIdColumn, $thisTable, "id", new CascadeStrategy())
->addInt($relIdA) ->foreignKey($otherIdColumn, $otherTable, "id", new CascadeStrategy())
->addInt($relIdB) ->unique($thisIdColumn, $otherIdColumn);
->foreignKey($relIdA, $tableNameA, "id", new CascadeStrategy())
->foreignKey($relIdB, $tableNameB, "id", new CascadeStrategy());
foreach ($dataColumns as $dataColumn) {
$query->addBool($dataColumn, false);
}
$query->unique(...$columns);
return $query;
}
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);
} }
public function dependsOn(): array { public function dependsOn(): array {
return [$this->handlerA->getTableName(), $this->handlerB->getTableName()]; return [$this->thisHandler->getTableName(), $this->otherHandler->getTableName()];
} }
public function getTableName(): string { public function getTableName(): string {
return self::buildTableName(...$this->dependsOn()); return $this->tableName;
} }
public function getCreateQueries(SQL $sql): array { public function getCreateQueries(SQL $sql): array {
return [$this->getTableQuery($sql)]; return [$this->getTableQuery($sql)];
} }
public function getProperties(DatabaseEntityHandler $handler): array {
return $this->properties[$handler->getTableName()];
}
public function getOtherHandler(DatabaseEntityHandler $handler): DatabaseEntityHandler { public function getOtherHandler(DatabaseEntityHandler $handler): DatabaseEntityHandler {
if ($handler === $this->handlerA) { if ($handler === $this->thisHandler) {
return $this->handlerB; return $this->thisHandler;
} else { } else {
return $this->handlerA; return $this->otherHandler;
} }
} }
} }

@ -0,0 +1,42 @@
<?php
namespace Core\Objects\DatabaseEntity\Controller;
use Core\Driver\SQL\SQL;
class NMRelationReference implements Persistable {
private DatabaseEntityHandler $handler;
private string $thisProperty;
private string $refProperty;
public function __construct(DatabaseEntityHandler $handler, string $thisProperty, string $refProperty) {
$this->handler = $handler;
$this->thisProperty = $thisProperty;
$this->refProperty = $refProperty;
}
public function dependsOn(): array {
return [$this->handler->getTableName()];
}
public function getTableName(): string {
return $this->handler->getTableName();
}
public function getCreateQueries(SQL $sql): array {
return []; // nothing to do here, will be managed by other handler
}
public function getThisProperty(): string {
return $this->thisProperty;
}
public function getRefProperty(): string {
return $this->refProperty;
}
public function getRelHandler(): DatabaseEntityHandler {
return $this->handler;
}
}

@ -29,7 +29,7 @@ class Group extends DatabaseEntity {
} }
public function getMembers(SQL $sql): array { public function getMembers(SQL $sql): array {
$nmTable = NMRelation::buildTableName(User::class, Group::class); $nmTable = User::getHandler($sql)->getNMRelation("groups")->getTableName();
$users = User::findBy(User::createBuilder($sql, false) $users = User::findBy(User::createBuilder($sql, false)
->innerJoin($nmTable, "user_id", "User.id") ->innerJoin($nmTable, "user_id", "User.id")
->whereEq("group_id", $this->id)); ->whereEq("group_id", $this->id));

@ -92,7 +92,7 @@ class User extends DatabaseEntity {
return [ return [
'id' => $this->getId(), 'id' => $this->getId(),
'username' => $this->name, 'username' => $this->name,
'language' => $this->language->getName(), 'language' => isset($this->language) ? $this->language->getName() : null,
]; ];
} }