web-base/Core/Objects/DatabaseEntity/Controller/DatabaseEntityQuery.class.php
2023-01-25 14:15:34 +01:00

225 lines
7.2 KiB
PHP

<?php
namespace Core\Objects\DatabaseEntity\Controller;
use Core\Driver\Logger\Logger;
use Core\Driver\SQL\Column\Column;
use Core\Driver\SQL\Expression\Alias;
use Core\Driver\SQL\Query\Select;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\NoFetch;
/**
* this class is similar to \Driver\SQL\Query\Select but with reduced functionality
* and more adapted to entities.
*/
class DatabaseEntityQuery extends Select {
const FETCH_NONE = 0;
const FETCH_DIRECT = 1;
const FETCH_RECURSIVE = 2;
private Logger $logger;
private DatabaseEntityHandler $handler;
private int $resultType;
private bool $logVerbose;
private array $additionalColumns;
private int $fetchSubEntities;
private ?DatabaseEntityQueryContext $context;
private function __construct(DatabaseEntityHandler $handler, int $resultType) {
parent::__construct($handler->getSQL(), ...$handler->getColumnNames());
$this->handler = $handler;
$this->logger = new Logger("DB-EntityQuery", $handler->getSQL());
$this->resultType = $resultType;
$this->logVerbose = false;
$this->context = null;
$this->additionalColumns = [];
$this->from($handler->getTableName());
$this->fetchSubEntities = self::FETCH_NONE;
if ($this->resultType === SQL::FETCH_ONE) {
$this->first();
}
}
public function only(array $fields): DatabaseEntityQuery {
if (!in_array("id", $fields)) {
$fields[] = "id";
}
$this->select(array_map(function ($field) {
return $this->handler->getColumnName($field);
}, $fields));
return $this;
}
public function withContext(DatabaseEntityQueryContext $context): self {
$this->context = $context;
return $this;
}
public function addCustomValue(mixed $selectValue): DatabaseEntityQuery {
if (is_string($selectValue)) {
$this->additionalColumns[] = $selectValue;
} else if ($selectValue instanceof Alias) {
$this->additionalColumns[] = $selectValue->getAlias();
} else if ($selectValue instanceof Column) {
$this->additionalColumns[] = $selectValue->getName();
} else {
$this->logger->debug("Cannot get selected column name from custom value of type: " . get_class($selectValue));
return $this;
}
$this->addSelectValue($selectValue);
return $this;
}
public function getHandler(): DatabaseEntityHandler {
return $this->handler;
}
public function debug(): DatabaseEntityQuery {
$this->logVerbose = true;
return $this;
}
public static function fetchAll(DatabaseEntityHandler $handler): DatabaseEntityQuery {
return new DatabaseEntityQuery($handler, SQL::FETCH_ALL);
}
public static function fetchOne(DatabaseEntityHandler $handler): DatabaseEntityQuery {
return new DatabaseEntityQuery($handler, SQL::FETCH_ONE);
}
public function fetchEntities(bool $recursive = false): DatabaseEntityQuery {
$this->fetchSubEntities = ($recursive ? self::FETCH_RECURSIVE : self::FETCH_DIRECT);
$visited = [$this->handler->getTableName()];
foreach ($this->handler->getRelations() as $propertyName => $relationHandler) {
$this->fetchRelation($propertyName, $this->handler->getTableName(), $this->handler, $relationHandler,
$recursive, "", $visited);
}
return $this;
}
// TODO: fix cycle detection + rather use deterministic aliases instead of t1, t2, t3...?
private function fetchRelation(string $propertyName, string $tableName, DatabaseEntityHandler $src, DatabaseEntityHandler $relationHandler,
bool $recursive = false, string $relationColumnPrefix = "", array &$visited = []) {
$property = $src->getProperty($propertyName);
if (DatabaseEntityHandler::getAttribute($property, NoFetch::class)) {
return;
}
$relIndex = count($visited);
if (in_array($relationHandler->getTableName(), $visited)) {
return;
} else {
$visited[] = $relationHandler->getTableName();
}
$columns = $src->getColumns();
$foreignColumn = $columns[$propertyName];
$foreignColumnName = $foreignColumn->getName();
$referencedTable = $relationHandler->getTableName();
$isNullable = !$foreignColumn->notNull();
$alias = "t$relIndex"; // t1, t2, t3, ...
if ($isNullable) {
$this->leftJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias);
} else {
$this->innerJoin($referencedTable, "$tableName.$foreignColumnName", "$alias.id", $alias);
}
$relationColumnPrefix .= DatabaseEntityHandler::buildColumnName($propertyName) . "_";
$recursiveRelations = $relationHandler->getRelations();
foreach ($relationHandler->getColumns() as $relPropertyName => $relColumn) {
$relColumnName = $relColumn->getName();
if (!isset($recursiveRelations[$relPropertyName]) || $recursive) {
$this->addValue("$alias.$relColumnName as $relationColumnPrefix$relColumnName");
if (isset($recursiveRelations[$relPropertyName]) && $recursive) {
$this->fetchRelation($relPropertyName, $alias, $relationHandler, $recursiveRelations[$relPropertyName],
$recursive, $relationColumnPrefix, $visited);
}
}
}
}
public function execute(): DatabaseEntity|array|null|false {
if ($this->logVerbose) {
$params = [];
$query = $this->build($params);
$this->logger->debug("QUERY: $query\nARGS: " . print_r($params, true));
}
$res = parent::execute();
if ($res === null || $res === false) {
return $res;
}
if ($this->resultType === SQL::FETCH_ALL) {
$entities = [];
$entitiesNM = [];
foreach ($res as $row) {
$cached = false;
$entity = null;
if ($this->context) {
$entity = $this->context->queryCache($this->handler, $row["id"]);
$cached = $entity !== null;
}
if (!$cached) {
$entity = $this->handler->entityFromRow($row, $this->additionalColumns, $this->fetchSubEntities, $this->context);
$this->context?->addCache($this->handler, $entity);
$entitiesNM[$entity->getId()] = $entity;
}
if ($entity) {
$entities[$entity->getId()] = $entity;
}
}
if (!empty($entitiesNM) && $this->fetchSubEntities !== self::FETCH_NONE) {
$this->handler->fetchNMRelations($entitiesNM, $this->fetchSubEntities, $this->context);
}
return $entities;
} else if ($this->resultType === SQL::FETCH_ONE) {
$cached = false;
$entity = null;
if ($this->context) {
$entity = $this->context->queryCache($this->handler, $res["id"]);
$cached = $entity !== null;
}
if (!$cached) {
$entity = $this->handler->entityFromRow($res, $this->additionalColumns, $this->fetchSubEntities, $this->context);
if ($entity instanceof DatabaseEntity && $this->fetchSubEntities !== self::FETCH_NONE) {
$this->handler->fetchNMRelations([$entity->getId() => $entity], $this->fetchSubEntities, $this->context);
}
$this->context?->addCache($this->handler, $entity);
}
return $entity;
} else {
$this->handler->getLogger()->error("Invalid result type for query builder, must be FETCH_ALL or FETCH_ONE");
return null;
}
}
public function executeSQL() {
return parent::execute();
}
}