DB Entity: Inheriting/Extending

This commit is contained in:
Roman Hergenreder
2022-11-27 12:33:27 +01:00
parent 3b2b5984d6
commit 26a22f5299
20 changed files with 308 additions and 157 deletions

View File

@@ -0,0 +1,18 @@
<?php
namespace Core\Objects\DatabaseEntity\Attribute;
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class ExtendingEnum extends EnumArr {
private array $mappings;
public function __construct(array $values) {
parent::__construct(array_keys($values));
$this->mappings = $values;
}
public function getMappings(): array {
return $this->mappings;
}
}

View File

@@ -32,7 +32,7 @@ abstract class DatabaseEntity {
return $handler->entityFromRow($row);
}
public static function newInstance(\ReflectionClass $reflectionClass, array $row) {
public static function newInstance(\ReflectionClass $reflectionClass) {
return $reflectionClass->newInstanceWithoutConstructor();
}
@@ -127,6 +127,12 @@ abstract class DatabaseEntity {
$class = $obj_or_class;
}
// if we are in an extending context, get the database handler for the root entity,
// as we do not persist attributes of the inheriting class
while ($class->getParentClass()->getName() !== DatabaseEntity::class) {
$class = $class->getParentClass();
}
$handler = self::$handlers[$class->getShortName()] ?? null;
if (!$handler) {
$handler = new DatabaseEntityHandler($sql, $class);

View File

@@ -10,7 +10,6 @@ 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\Compare;
use Core\Driver\SQL\Condition\CondAnd;
use Core\Driver\SQL\Condition\CondBool;
use Core\Driver\SQL\Condition\CondIn;
@@ -33,12 +32,12 @@ use Core\Driver\SQL\Type\CurrentColumn;
use Core\Driver\SQL\Type\CurrentTable;
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\Transient;
use Core\Objects\DatabaseEntity\Attribute\Unique;
use PHPUnit\Util\Exception;
class DatabaseEntityHandler implements Persistable {
@@ -49,6 +48,8 @@ class DatabaseEntityHandler implements Persistable {
private array $relations;
private array $constraints;
private array $nmRelations;
private array $extendingClasses;
private ?\ReflectionProperty $extendingProperty;
private SQL $sql;
private Logger $logger;
@@ -66,7 +67,9 @@ class DatabaseEntityHandler implements Persistable {
$this->properties = []; // property name => \ReflectionProperty
$this->relations = []; // property name => DatabaseEntityHandler
$this->constraints = []; // \Driver\SQL\Constraint\Constraint
$this->nmRelations = []; // table name => NMRelation
$this->nmRelations = []; // table name => NMRelation
$this->extendingClasses = []; // enum value => \ReflectionClass
$this->extendingProperty = null; // only one attribute can hold the type of the extending class
foreach ($this->entityClass->getProperties() as $property) {
$propertyName = $property->getName();
@@ -91,6 +94,36 @@ class DatabaseEntityHandler implements Persistable {
continue;
}
$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);
if (!$requestedClass->isSubclassOf($this->entityClass)) {
$this->raiseError("Class '$extendingClass' must be an inheriting from '" . $this->entityClass->getName() . "' for an extending enum");
} else {
$this->extendingClasses[$key] = $requestedClass;
}
} catch (\ReflectionException $ex) {
$this->raiseError("Cannot persist extending enum for class $extendingClass: " . $ex->getMessage());
}
}
}
}
$defaultValue = (self::getAttribute($property, DefaultValue::class))?->getValue();
$isUnique = !empty($property->getAttributes(Unique::class));
@@ -112,18 +145,6 @@ class DatabaseEntityHandler implements Persistable {
$this->columns[$propertyName] = new BoolColumn($columnName, $defaultValue ?? false);
} else if ($propertyTypeName === 'DateTime') {
$this->columns[$propertyName] = new DateTimeColumn($columnName, $nullable, $defaultValue);
/*} else if ($propertyName === 'array') {
$many = self::getAttribute($property, Many::class);
if ($many) {
$requestedType = $many->getValue();
if (isClass($requestedType)) {
$requestedClass = new \ReflectionClass($requestedType);
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $requestedType");
}
} else {
$this->raiseError("Cannot persist class '$className': Property '$propertyName' has non persist-able type: $propertyTypeName");
}*/
} else if ($propertyTypeName === "array") {
$multiple = self::getAttribute($property, Multiple::class);
if (!$multiple) {
@@ -248,42 +269,62 @@ class DatabaseEntityHandler implements Persistable {
return $rel_row;
}
private function getValueFromRow(array $row, string $propertyName, mixed &$value): bool {
$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) {
$value = new \DateTime($value);
} else if ($column instanceof JsonColumn) {
$value = json_decode($value);
} else if (isset($this->relations[$propertyName])) {
$relColumnPrefix = self::getColumnName($propertyName) . "_";
if (array_key_exists($relColumnPrefix . "id", $row)) {
$relId = $row[$relColumnPrefix . "id"];
if ($relId !== null) {
$relationHandler = $this->relations[$propertyName];
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix));
} else if (!$column->notNull()) {
$value = null;
} else {
return false;
}
} else {
return false;
}
}
return true;
}
public function entityFromRow(array $row): ?DatabaseEntity {
try {
$entity = call_user_func($this->entityClass->getName() . "::newInstance", $this->entityClass, $row);
$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];
}
}
}
$entity = call_user_func($constructorClass->getName() . "::newInstance", $constructorClass);
if (!($entity instanceof DatabaseEntity)) {
$this->logger->error("Created Object is not of type DatabaseEntity");
return null;
}
foreach ($this->columns as $propertyName => $column) {
$columnName = $column->getName();
if (array_key_exists($columnName, $row)) {
$value = $row[$columnName];
$property = $this->properties[$propertyName];
if ($column instanceof DateTimeColumn) {
$value = new \DateTime($value);
} else if ($column instanceof JsonColumn) {
$value = json_decode($value);
} else if (isset($this->relations[$propertyName])) {
$relColumnPrefix = self::getColumnName($propertyName) . "_";
if (array_key_exists($relColumnPrefix . "id", $row)) {
$relId = $row[$relColumnPrefix . "id"];
if ($relId !== null) {
$relationHandler = $this->relations[$propertyName];
$value = $relationHandler->entityFromRow(self::getPrefixedRow($row, $relColumnPrefix));
} else if (!$column->notNull()) {
$value = null;
} else {
continue;
}
} else {
continue;
}
}
foreach ($this->properties as $property) {
if ($this->getValueFromRow($row, $property->getName(), $value)) {
$property->setAccessible(true);
$property->setValue($entity, $value);
}
@@ -449,7 +490,7 @@ class DatabaseEntityHandler implements Persistable {
}
}
$rows = $relEntityQuery->execute();
$rows = $relEntityQuery->executeSQL();
if (!is_array($rows)) {
$this->logger->error("Error fetching n:m relations from table: '$nmTable': " . $this->sql->getLastError());
return;
@@ -698,7 +739,7 @@ class DatabaseEntityHandler implements Persistable {
private function raiseError(string $message) {
$this->logger->error($message);
throw new Exception($message);
throw new \Exception($message);
}
public function getSQL(): SQL {
@@ -713,6 +754,9 @@ class DatabaseEntityHandler implements Persistable {
$firstEntity = (is_array($entities) ? current($entities) : $entities);
$firstRow = $this->prepareRow($firstEntity, "insert");
if ($firstRow === false) {
return null;
}
$statement = $this->sql->insert($this->tableName, array_keys($firstRow))
->addRow(...array_values($firstRow));

View File

@@ -137,4 +137,8 @@ class DatabaseEntityQuery extends Select {
return null;
}
}
public function executeSQL() {
return parent::execute();
}
}

View File

@@ -0,0 +1,207 @@
<?php
namespace Core\Objects\DatabaseEntity;
use Core\API\Parameter\Parameter;
use Core\Objects\DatabaseEntity\Attribute\DefaultValue;
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\DatabaseEntity\Attribute\Unique;
use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
use Core\Objects\Router\DocumentRoute;
use Core\Objects\Router\RedirectRoute;
use Core\Objects\Router\Router;
use Core\Objects\Router\StaticFileRoute;
abstract class Route extends DatabaseEntity {
const PARAMETER_PATTERN = "/^{([^:]+)(:(.*?)(\?)?)?}$/";
const ROUTE_TYPES = [
"redirect_temporary" => RedirectRoute::class,
"redirect_permanently" => RedirectRoute::class,
"static" => StaticFileRoute::class,
"dynamic" => DocumentRoute::class
];
#[MaxLength(128)]
#[Unique]
private string $pattern;
#[ExtendingEnum(self::ROUTE_TYPES)]
private string $type;
#[MaxLength(128)]
private string $target;
#[MaxLength(64)]
protected ?string $extra;
#[DefaultValue(true)]
private bool $active;
private bool $exact;
public function __construct(string $type, string $pattern, string $target, bool $exact = true) {
parent::__construct();
$this->target = $target;
$this->pattern = $pattern;
$this->exact = $exact;
$this->type = $type;
$this->active = true;
}
private static function parseParamType(?string $type): ?int {
if ($type === null || trim($type) === "") {
return null;
}
$type = strtolower(trim($type));
if (in_array($type, ["int", "integer"])) {
return Parameter::TYPE_INT;
} else if (in_array($type, ["float", "double"])) {
return Parameter::TYPE_FLOAT;
} else if (in_array($type, ["bool", "boolean"])) {
return Parameter::TYPE_BOOLEAN;
} else {
return Parameter::TYPE_STRING;
}
}
public function getPattern(): string {
return $this->pattern;
}
public function getTarget(): string {
return $this->target;
}
public abstract function call(Router $router, array $params): string;
protected function getArgs(): array {
return [$this->pattern, $this->exact];
}
public function getClass(): \ReflectionClass {
return new \ReflectionClass($this);
}
public function generateCache(): string {
$reflection = $this->getClass();
$className = $reflection->getShortName();
$args = implode(", ", array_map(function ($arg) {
return var_export($arg, true);
}, $this->getArgs()));
return "new $className($args)";
}
public function match(string $url) {
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
$countPattern = count($patternParts);
$patternOffset = 0;
# /test/param/optional/123
$urlParts = explode("/", Router::cleanURL($url));
$countUrl = count($urlParts);
$urlOffset = 0;
$params = [];
for (; $patternOffset < $countPattern; $patternOffset++) {
if (!preg_match(self::PARAMETER_PATTERN, $patternParts[$patternOffset], $match)) {
// not a parameter? check if it matches
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
return false;
}
$urlOffset++;
} else {
// we got a parameter here
$paramName = $match[1];
if (isset($match[2])) {
$paramType = self::parseParamType($match[3]) ?? Parameter::TYPE_MIXED;
$paramOptional = !empty($match[4] ?? null);
} else {
$paramType = Parameter::TYPE_MIXED;
$paramOptional = false;
}
$parameter = new Parameter($paramName, $paramType, $paramOptional);
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
if ($parameter->optional) {
$value = $urlParts[$urlOffset] ?? null;
if ($value === null || $value === "") {
$params[$paramName] = null;
} else {
if (!$parameter->parseParam($value)) {
return false;
} else {
$params[$paramName] = $parameter->value;
}
}
if ($urlOffset < $countUrl) {
$urlOffset++;
}
} else {
return false;
}
} else {
$value = $urlParts[$urlOffset];
if (!$parameter->parseParam($value)) {
return false;
} else {
$params[$paramName] = $parameter->value;
$urlOffset++;
}
}
}
}
if ($urlOffset !== $countUrl && $this->exact) {
return false;
}
return $params;
}
public function getUrl(array $parameters = []): string {
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
foreach ($patternParts as $i => $part) {
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
$paramName = $match[1];
$patternParts[$i] = $parameters[$paramName] ?? null;
}
}
return "/" . implode("/", array_filter($patternParts));
}
public function getParameterNames(): array {
$parameterNames = [];
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
foreach ($patternParts as $part) {
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
$parameterNames[] = $match[1];
}
}
return $parameterNames;
}
public function jsonSerialize(): array {
return [
"id" => $this->getId(),
"pattern" => $this->pattern,
"type" => $this->type,
"target" => $this->target,
"extra" => $this->extra,
"exact" => $this->exact,
"active" => $this->active,
];
}
}

View File

@@ -3,7 +3,7 @@
namespace Core\Objects\DatabaseEntity;
use Core\Driver\SQL\SQL;
use Core\Objects\DatabaseEntity\Attribute\Enum;
use Core\Objects\DatabaseEntity\Attribute\ExtendingEnum;
use Core\Objects\DatabaseEntity\Attribute\MaxLength;
use Core\Objects\TwoFactor\KeyBasedTwoFactorToken;
use Core\Objects\TwoFactor\TimeBasedTwoFactorToken;
@@ -11,7 +11,12 @@ use Core\Objects\DatabaseEntity\Controller\DatabaseEntity;
abstract class TwoFactorToken extends DatabaseEntity {
#[Enum('totp','fido')] private string $type;
const TWO_FACTOR_TOKEN_TYPES = [
"totp" => TimeBasedTwoFactorToken::class,
"fido" => KeyBasedTwoFactorToken::class,
];
#[ExtendingEnum(self::TWO_FACTOR_TOKEN_TYPES)] private string $type;
private bool $confirmed;
private bool $authenticated;
#[MaxLength(512)] private string $data;
@@ -62,17 +67,6 @@ abstract class TwoFactorToken extends DatabaseEntity {
return $this->id;
}
public static function newInstance(\ReflectionClass $reflectionClass, array $row) {
if ($row["type"] === TimeBasedTwoFactorToken::TYPE) {
return (new \ReflectionClass(TimeBasedTwoFactorToken::class))->newInstanceWithoutConstructor();
} else if ($row["type"] === KeyBasedTwoFactorToken::TYPE) {
return (new \ReflectionClass(KeyBasedTwoFactorToken::class))->newInstanceWithoutConstructor();
} else {
// TODO: error message
return null;
}
}
public function isAuthenticated(): bool {
return $this->authenticated;
}