DB Entity: Inheriting/Extending
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -137,4 +137,8 @@ class DatabaseEntityQuery extends Select {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function executeSQL() {
|
||||
return parent::execute();
|
||||
}
|
||||
}
|
||||
207
Core/Objects/DatabaseEntity/Route.class.php
Normal file
207
Core/Objects/DatabaseEntity/Route.class.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user