diff --git a/Core/API/DatabaseAPI.class.php b/Core/API/DatabaseAPI.class.php index f3886f2..7252003 100644 --- a/Core/API/DatabaseAPI.class.php +++ b/Core/API/DatabaseAPI.class.php @@ -13,6 +13,7 @@ namespace Core\API\Database { use Core\API\DatabaseAPI; use Core\API\Parameter\StringType; use Core\Objects\Context; + use Core\Objects\DatabaseEntity\Controller\DatabaseEntity; class Status extends DatabaseAPI { @@ -48,6 +49,10 @@ namespace Core\API\Database { $classPath = "\\$baseDir\\Objects\\DatabaseEntity\\$className"; if (isClass($classPath)) { $class = new \ReflectionClass($classPath); + if (!$class->isSubclassOf(DatabaseEntity::class)) { + $class = null; + continue; + } break; } } diff --git a/Core/API/GroupsAPI.class.php b/Core/API/GroupsAPI.class.php index 599f255..c8e2c51 100644 --- a/Core/API/GroupsAPI.class.php +++ b/Core/API/GroupsAPI.class.php @@ -62,8 +62,9 @@ namespace Core\API\Groups { return false; } + $nmTable = User::getHandler($sql)->getNMRelation("groups")->getTableName(); $memberCount = new Alias($sql->select(new Count()) - ->from(NMRelation::buildTableName("User", "Group")) + ->from($nmTable) ->whereEq("group_id", new Column("Group.id")), "memberCount"); $groupsQuery = $this->createPaginationQuery($sql, [$memberCount]); @@ -119,7 +120,7 @@ namespace Core\API\Groups { protected function _execute(): bool { $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")); $nmJoin = new InnerJoin($nmTable, "$nmTable.user_id", "User.id"); if (!$this->initPagination($sql, User::class, $condition, 100, [$nmJoin])) { diff --git a/Core/Driver/SQL/Condition/CondIn.class.php b/Core/Driver/SQL/Condition/CondIn.class.php index 1ef02e7..1c2097f 100644 --- a/Core/Driver/SQL/Condition/CondIn.class.php +++ b/Core/Driver/SQL/Condition/CondIn.class.php @@ -30,7 +30,7 @@ class CondIn extends Condition { $values = implode(",", $values); $values = "($values)"; } else if($haystack instanceof Select) { - $values = $haystack->build($params); + $values = $haystack->getExpression($sql, $params); } else { $sql->getLogger()->error("Unsupported in-expression value: " . get_class($haystack)); return false; diff --git a/Core/Objects/DatabaseEntity/Attribute/BigInt.class.php b/Core/Objects/DatabaseEntity/Attribute/BigInt.class.php new file mode 100644 index 0000000..7582eaf --- /dev/null +++ b/Core/Objects/DatabaseEntity/Attribute/BigInt.class.php @@ -0,0 +1,16 @@ +unsigned = $unsigned; + } + + public function isUnsigned(): bool { + return $this->unsigned; + } +} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Attribute/Multiple.php b/Core/Objects/DatabaseEntity/Attribute/Multiple.php index ec62963..13bc607 100644 --- a/Core/Objects/DatabaseEntity/Attribute/Multiple.php +++ b/Core/Objects/DatabaseEntity/Attribute/Multiple.php @@ -14,5 +14,4 @@ class Multiple { public function getClassName(): string { return $this->className; } - } \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Attribute/MultipleReference.class.php b/Core/Objects/DatabaseEntity/Attribute/MultipleReference.class.php new file mode 100644 index 0000000..036faff --- /dev/null +++ b/Core/Objects/DatabaseEntity/Attribute/MultipleReference.class.php @@ -0,0 +1,32 @@ +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; + } +} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Attribute/Unique.class.php b/Core/Objects/DatabaseEntity/Attribute/Unique.class.php index 4b69504..f3a13bb 100644 --- a/Core/Objects/DatabaseEntity/Attribute/Unique.class.php +++ b/Core/Objects/DatabaseEntity/Attribute/Unique.class.php @@ -2,6 +2,15 @@ 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; + } } \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php index 4f9d6a9..5ddd590 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntity.class.php @@ -226,6 +226,7 @@ abstract class DatabaseEntity implements ArrayAccess, JsonSerializable { if (!$handler || $allowOverride) { $handler = new DatabaseEntityHandler($sql, $class); self::$handlers[$class->getShortName()] = $handler; + $handler->init(); } return $handler; diff --git a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php index 22b70bb..079da5b 100644 --- a/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php +++ b/Core/Objects/DatabaseEntity/Controller/DatabaseEntityHandler.php @@ -3,6 +3,7 @@ namespace Core\Objects\DatabaseEntity\Controller; use Core\Driver\Logger\Logger; +use Core\Driver\SQL\Column\BigIntColumn; use Core\Driver\SQL\Column\BoolColumn; use Core\Driver\SQL\Column\Column; 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\JsonColumn; 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\Condition; use Core\Driver\SQL\Column\DoubleColumn; use Core\Driver\SQL\Column\FloatColumn; use Core\Driver\SQL\Condition\CondNot; -use Core\Driver\SQL\Condition\CondOr; use Core\Driver\SQL\Constraint\ForeignKey; use Core\Driver\SQL\Join\InnerJoin; 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\Type\CurrentColumn; use Core\Driver\SQL\Type\CurrentTable; +use Core\Objects\DatabaseEntity\Attribute\BigInt; use Core\Objects\DatabaseEntity\Attribute\Enum; use Core\Objects\DatabaseEntity\Attribute\DefaultValue; use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum; use Core\Objects\DatabaseEntity\Attribute\Json; use Core\Objects\DatabaseEntity\Attribute\MaxLength; use Core\Objects\DatabaseEntity\Attribute\Multiple; +use Core\Objects\DatabaseEntity\Attribute\MultipleReference; use Core\Objects\DatabaseEntity\Attribute\Transient; use Core\Objects\DatabaseEntity\Attribute\Unique; @@ -56,6 +56,7 @@ class DatabaseEntityHandler implements Persistable { 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)) { @@ -70,6 +71,13 @@ class DatabaseEntityHandler implements Persistable { $this->nmRelations = []; // table name => NMRelation $this->extendingClasses = []; // enum value => \ReflectionClass $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) { $propertyName = $property->getName(); @@ -132,8 +140,13 @@ class DatabaseEntityHandler implements Persistable { 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); + $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); + } } } else if ($propertyTypeName === 'int') { $this->columns[$propertyName] = new IntColumn($columnName, $nullable, $defaultValue); @@ -152,25 +165,26 @@ class DatabaseEntityHandler implements Persistable { } else { $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. " . - "Is the 'Multiple' attribute missing?"); + "Is the 'Multiple' or 'MultipleReference' attribute missing?"); } try { - $refClass = $multiple->getClassName(); + $refClass = $multiple ? $multiple->getClassName() : $multipleReference->getClassName(); $requestedClass = new \ReflectionClass($refClass); if ($requestedClass->isSubclassOf(DatabaseEntity::class)) { - $nmTableName = NMRelation::buildTableName($this->getTableName(), $requestedClass->getShortName()); - $nmRelation = $this->nmRelations[$nmTableName] ?? null; - if (!$nmRelation) { - $otherHandler = DatabaseEntity::getHandler($this->sql, $requestedClass); - $otherNM = $otherHandler->getNMRelations(); - $nmRelation = $otherNM[$nmTableName] ?? (new NMRelation($this, $otherHandler)); - $this->nmRelations[$nmTableName] = $nmRelation; + $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; } - - $this->nmRelations[$nmTableName]->addProperty($this, $property); } else { $this->raiseError("Cannot persist class '$className': Property '$propertyName' of type multiple can " . "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); $attribute = array_shift($attributes); return $attribute?->newInstance(); @@ -241,10 +259,6 @@ class DatabaseEntityHandler implements Persistable { return $this->relations; } - public function getNMRelations(): array { - return $this->nmRelations; - } - public function getColumnName(string $property): string { if ($property === "id") { return "$this->tableName.id"; @@ -267,12 +281,20 @@ class DatabaseEntityHandler implements Persistable { } public function dependsOn(): array { - $foreignTables = array_map(function (DatabaseEntityHandler $relationHandler) { - return $relationHandler->getTableName(); - }, $this->relations); + $foreignTables = array_filter(array_map( + function (DatabaseEntityHandler $relationHandler) { + return $relationHandler->getTableName(); + }, $this->relations), + function ($tableName) { + return $tableName !== $this->getTableName(); + }); return array_unique($foreignTables); } + public function getNMRelation(string $property): Persistable { + return $this->nmRelations[$property]; + } + public static function getPrefixedRow(array $row, string $prefix): array { $rel_row = []; foreach ($row as $relKey => $relValue) { @@ -355,11 +377,10 @@ class DatabaseEntityHandler implements Persistable { } // init n:m / 1:n properties with empty arrays - foreach ($this->nmRelations as $nmRelation) { - foreach ($nmRelation->getProperties($this) as $property) { - $property->setAccessible(true); - $property->setValue($entity, []); - } + foreach ($this->nmRelations as $propertyName => $nmRelation) { + $property = $this->properties[$propertyName]; + $property->setAccessible(true); + $property->setValue($entity, []); } $this->properties["id"]->setAccessible(true); @@ -377,57 +398,32 @@ class DatabaseEntityHandler implements Persistable { return true; } - foreach ($this->nmRelations as $nmTable => $nmRelation) { - - $thisIdColumn = $nmRelation->getIdColumn($this); - $thisTableName = $this->getTableName(); - $dataColumns = $nmRelation->getDataColumns(); - $otherHandler = $nmRelation->getOtherHandler($this); - $refIdColumn = $nmRelation->getIdColumn($otherHandler); + 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) { + $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 - $doDelete = true; $deleteStatement = $this->sql->delete($nmTable) ->whereEq($thisIdColumn, $entity->getId()); // this condition is important - if (!empty($dataColumns)) { - $conditions = []; - $doDelete = false; - foreach ($dataColumns[$thisTableName] as $propertyName => $columnName) { - if ($properties !== null && !in_array($propertyName, $properties)) { - 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 ($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)) + ); } - - 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(); } } @@ -442,52 +438,52 @@ class DatabaseEntityHandler implements Persistable { } $success = true; - foreach ($this->nmRelations as $nmTable => $nmRelation) { - $otherHandler = $nmRelation->getOtherHandler($this); - $thisIdColumn = $nmRelation->getIdColumn($this); - $thisTableName = $this->getTableName(); - $refIdColumn = $nmRelation->getIdColumn($otherHandler); - $dataColumns = $nmRelation->getDataColumns(); + foreach ($this->nmRelations as $nmProperty => $nmRelation) { - $columns = [ - $thisIdColumn, - $refIdColumn, - ]; - - if (!empty($dataColumns)) { - $columns = array_merge($columns, array_values($dataColumns[$thisTableName])); + if ($properties !== null && !in_array($nmProperty, $properties)) { + continue; } - $statement = $this->sql->insert($nmTable, $columns); - if ($ignoreExisting) { - $statement->onDuplicateKeyStrategy(new UpdateStrategy($nmRelation->getAllColumns(), [ - $thisIdColumn => $entity->getId() - ])); + if ($nmRelation instanceof NMRelation) { + $otherHandler = $nmRelation->getOtherHandler($this); + $thisIdColumn = $nmRelation->getIdColumn($this); + $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; - foreach ($nmRelation->getProperties($this) as $property) { - if ($properties !== null && !in_array($property->getName(), $properties)) { - continue; - } - - $property->setAccessible(true); - $relEntities = $property->getValue($entity); - foreach ($relEntities as $relEntity) { - $relEntityId = (is_int($relEntity) ? $relEntity : $relEntity->getId()); - $nmRow = [$entity->getId(), $relEntityId]; - if (!empty($dataColumns)) { - foreach (array_keys($dataColumns[$thisTableName]) as $propertyName) { - $nmRow[] = $property->getName() === $propertyName; - } + $property = $this->properties[$nmProperty]; + $property->setAccessible(true); + $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() + ])); } - $statement->addRow(...$nmRow); - $doInsert = true; - } - } + 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()]; + $thisIdProperty->setAccessible(true); - if ($doInsert) { - $success = $statement->execute() && $success; + foreach ($relEntities as $relEntity) { + $thisIdProperty->setValue($relEntity, $entity); + } + + $success = $otherHandler->getInsertQuery($relEntities)->execute() && $success; + } } } @@ -500,7 +496,7 @@ class DatabaseEntityHandler implements Persistable { foreach ($entities as $entity) { foreach ($this->relations as $propertyName => $relHandler) { $property = $this->properties[$propertyName]; - if ($property->isInitialized($entity) || true) { + if ($property->isInitialized($entity)) { $relEntity = $this->properties[$propertyName]->getValue($entity); if ($relEntity) { $relHandler->fetchNMRelations([$relEntity->getId() => $relEntity], true); @@ -515,66 +511,66 @@ class DatabaseEntityHandler implements Persistable { } $entityIds = array_keys($entities); - foreach ($this->nmRelations as $nmTable => $nmRelation) { - $otherHandler = $nmRelation->getOtherHandler($this); + foreach ($this->nmRelations as $nmProperty => $nmRelation) { + $nmTable = $nmRelation->getTableName(); + $property = $this->properties[$nmProperty]; + $property->setAccessible(true); - $thisIdColumn = $nmRelation->getIdColumn($this); - $thisProperties = $nmRelation->getProperties($this); - $thisTableName = $this->getTableName(); + if ($nmRelation instanceof NMRelation) { + $thisIdColumn = $nmRelation->getIdColumn($this); + $otherHandler = $nmRelation->getOtherHandler($this); + $refIdColumn = $nmRelation->getIdColumn($otherHandler); + $refTableName = $otherHandler->getTableName(); - $refIdColumn = $nmRelation->getIdColumn($otherHandler); - $refProperties = $nmRelation->getProperties($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)); - $dataColumns = $nmRelation->getDataColumns(); - - $relEntityQuery = DatabaseEntityQuery::fetchAll($otherHandler) - ->addJoin(new InnerJoin($nmTable, "$nmTable.$refIdColumn", "$refTableName.id")) - ->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; + $rows = $relEntityQuery->executeSQL(); + if (!is_array($rows)) { + $this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError()); + return; } - $thisEntity = $entities[$row[$thisIdColumn]]; - $relEntity = $relEntities[$relId]; - $mappings = [ - [$refProperties, $refTableName, $relEntity, $thisEntity], - [$thisProperties, $thisTableName, $thisEntity, $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); - } + $relEntities = []; + foreach ($rows as $row) { + $relId = $row["id"]; + if (!isset($relEntities[$relId])) { + $relEntity = $otherHandler->entityFromRow($row, [], $recursive); + $relEntities[$relId] = $relEntity; } + + $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); } } } diff --git a/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php b/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php index 69fc281..de48ae7 100644 --- a/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php +++ b/Core/Objects/DatabaseEntity/Controller/NMRelation.class.php @@ -2,135 +2,65 @@ namespace Core\Objects\DatabaseEntity\Controller; -# TODO: Allow more than 2 relations here? - use Core\Driver\SQL\Query\CreateTable; use Core\Driver\SQL\SQL; use Core\Driver\SQL\Strategy\CascadeStrategy; class NMRelation implements Persistable { - private DatabaseEntityHandler $handlerA; - private DatabaseEntityHandler $handlerB; - private array $properties; + private DatabaseEntityHandler $thisHandler; + private DatabaseEntityHandler $otherHandler; + private \ReflectionProperty $property; + private string $tableName; - public function __construct(DatabaseEntityHandler $handlerA, DatabaseEntityHandler $handlerB) { - $this->handlerA = $handlerA; - $this->handlerB = $handlerB; - $tableNameA = $handlerA->getTableName(); - $tableNameB = $handlerB->getTableName(); - if ($tableNameA === $tableNameB) { - 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 __construct(DatabaseEntityHandler $thisHandler, \ReflectionProperty $thisProperty, DatabaseEntityHandler $otherHandler) { + $this->thisHandler = $thisHandler; + $this->otherHandler = $otherHandler; + $this->property = $thisProperty; + $this->tableName = "NM_" . $thisHandler->getTableName() . "_" . + DatabaseEntityHandler::buildColumnName($thisProperty->getName()); } public function getIdColumn(DatabaseEntityHandler $handler): string { return DatabaseEntityHandler::buildColumnName($handler->getTableName()) . "_id"; } - public function getDataColumns(): array { - - $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 getProperty(): \ReflectionProperty { + return $this->property; } public function getTableQuery(SQL $sql): CreateTable { - $tableNameA = $this->handlerA->getTableName(); - $tableNameB = $this->handlerB->getTableName(); + $thisTable = $this->thisHandler->getTableName(); + $otherTable = $this->otherHandler->getTableName(); + $thisIdColumn = $this->getIdColumn($this->thisHandler); + $otherIdColumn = $this->getIdColumn($this->otherHandler); - $columns = $this->getAllColumns(); - list ($relIdA, $relIdB) = $columns; - $dataColumns = array_slice($columns, 2); - $query = $sql->createTable(self::buildTableName($tableNameA, $tableNameB)) - ->addInt($relIdA) - ->addInt($relIdB) - ->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); + return $sql->createTable($this->tableName) + ->addInt($thisIdColumn) + ->addInt($otherIdColumn) + ->foreignKey($thisIdColumn, $thisTable, "id", new CascadeStrategy()) + ->foreignKey($otherIdColumn, $otherTable, "id", new CascadeStrategy()) + ->unique($thisIdColumn, $otherIdColumn); } public function dependsOn(): array { - return [$this->handlerA->getTableName(), $this->handlerB->getTableName()]; + return [$this->thisHandler->getTableName(), $this->otherHandler->getTableName()]; } public function getTableName(): string { - return self::buildTableName(...$this->dependsOn()); + return $this->tableName; } public function getCreateQueries(SQL $sql): array { return [$this->getTableQuery($sql)]; } - public function getProperties(DatabaseEntityHandler $handler): array { - return $this->properties[$handler->getTableName()]; - } - public function getOtherHandler(DatabaseEntityHandler $handler): DatabaseEntityHandler { - if ($handler === $this->handlerA) { - return $this->handlerB; + if ($handler === $this->thisHandler) { + return $this->thisHandler; } else { - return $this->handlerA; + return $this->otherHandler; } } } \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Controller/NMRelationReference.class.php b/Core/Objects/DatabaseEntity/Controller/NMRelationReference.class.php new file mode 100644 index 0000000..081b6e1 --- /dev/null +++ b/Core/Objects/DatabaseEntity/Controller/NMRelationReference.class.php @@ -0,0 +1,42 @@ +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; + } +} \ No newline at end of file diff --git a/Core/Objects/DatabaseEntity/Group.class.php b/Core/Objects/DatabaseEntity/Group.class.php index c4203f9..25e7f22 100644 --- a/Core/Objects/DatabaseEntity/Group.class.php +++ b/Core/Objects/DatabaseEntity/Group.class.php @@ -29,7 +29,7 @@ class Group extends DatabaseEntity { } 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) ->innerJoin($nmTable, "user_id", "User.id") ->whereEq("group_id", $this->id)); diff --git a/Core/Objects/DatabaseEntity/User.class.php b/Core/Objects/DatabaseEntity/User.class.php index 7fe85ea..068575f 100644 --- a/Core/Objects/DatabaseEntity/User.class.php +++ b/Core/Objects/DatabaseEntity/User.class.php @@ -92,7 +92,7 @@ class User extends DatabaseEntity { return [ 'id' => $this->getId(), 'username' => $this->name, - 'language' => $this->language->getName(), + 'language' => isset($this->language) ? $this->language->getName() : null, ]; }