chore: complete update

This commit is contained in:
2025-07-17 16:24:20 +02:00
parent 899227b0a4
commit 64a7051137
1300 changed files with 85570 additions and 2756 deletions

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Metadata;
use ReflectionClass;
final readonly class EntityMetadata
{
public function __construct(
public string $entityClass,
public string $tableName,
public string $idColumn,
public string $idProperty,
public ReflectionClass $reflection,
public array $properties
) {}
public function getProperty(string $name): ?PropertyMetadata
{
return $this->properties[$name] ?? null;
}
public function getColumnName(string $propertyName): string
{
$property = $this->getProperty($propertyName);
return $property?->columnName ?? $propertyName;
}
}

View File

@@ -0,0 +1,501 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Metadata;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Type;
use App\Framework\Database\Exception\DatabaseException;
use ReflectionClass;
use ReflectionProperty;
use ReflectionParameter;
use ReflectionNamedType;
use ReflectionUnionType;
final class MetadataExtractor
{
public function extractMetadata(string $entityClass): EntityMetadata
{
try {
$reflection = new ReflectionClass($entityClass);
$entityAttribute = $this->getEntityAttribute($reflection);
if (!$entityAttribute) {
throw new DatabaseException("Class {$entityClass} is not marked as Entity");
}
$tableName = $entityAttribute->tableName ?? $this->getTableNameFromClass($entityClass);
$idColumn = $entityAttribute->idColumn ?? 'id';
$properties = $this->extractProperties($reflection);
$relations = $this->extractRelations($reflection);
$idProperty = $this->findIdProperty($reflection, $properties, $idColumn);
return new EntityMetadata(
entityClass: $entityClass,
tableName: $tableName,
idColumn: $idColumn,
idProperty: $idProperty,
reflection: $reflection,
properties: array_merge($properties, $relations)
);
} catch (\ReflectionException $e) {
throw new DatabaseException("Failed to analyze entity {$entityClass}: {$e->getMessage()}", 0, $e);
}
}
private function extractProperties(ReflectionClass $reflection): array
{
$properties = [];
$constructor = $reflection->getConstructor();
if (!$constructor) {
return $properties;
}
foreach ($constructor->getParameters() as $param) {
$propertyMetadata = $this->extractPropertyMetadata($param, $reflection);
$properties[$param->getName()] = $propertyMetadata;
}
return $properties;
}
/**
* Extrahiert Relation-Properties die nicht im Constructor sind
*/
private function extractRelations(ReflectionClass $reflection): array
{
$relations = [];
foreach ($reflection->getProperties() as $property) {
if ($property->isPromoted()) {
continue; // Überspringen, da bereits in extractProperties erfasst
}
$typeAttribute = $this->getTypeAttribute($property);
if ($typeAttribute) {
$relationMetadata = $this->extractRelationMetadata($property, $typeAttribute, $reflection);
$relations[$property->getName()] = $relationMetadata;
continue;
}
// Prüfe ob der Property-Typ eine Entity-Klasse ist
$propertyType = $property->getType();
if ($propertyType instanceof \ReflectionNamedType) {
$typeName = $propertyType->getName();
// Prüfe ob die Typ-Klasse eine Entity ist
if (class_exists($typeName) && $this->isEntityClass($typeName)) {
$relationMetadata = $this->createRelationMetadataForEntityProperty($property, $typeName, $reflection);
$relations[$property->getName()] = $relationMetadata;
continue;
}
}
}
return $relations;
}
private function isEntityClass(string $className): bool
{
try {
$classReflection = new \ReflectionClass($className);
return $this->getEntityAttribute($classReflection) !== null;
} catch (\ReflectionException) {
return false;
}
}
private function createRelationMetadataForEntityProperty(
\ReflectionProperty $property,
string $targetClass,
\ReflectionClass $parentReflection
): PropertyMetadata {
$propertyName = $property->getName();
$relationType = $this->determineRelationType($property);
if($relationType === 'belongsTo') {
return $this->createBelongsToRelationMetadata($property, $targetClass, $parentReflection);
}
return $this->createHasRelationMetadata($property, $targetClass, $parentReflection, $relationType);
/*
// Foreign Key ist der Primary Key der Ziel-Entity
$targetReflection = new \ReflectionClass($targetClass);
$targetEntityAttribute = $this->getEntityAttribute($targetReflection);
$foreignKey = $targetEntityAttribute?->idColumn ?? 'id';
// Local Key ist die entsprechende Spalte in der aktuellen Entity
// Für "image" Property -> "image_id"
$localKey = $propertyName . '_id';
// Typ-Analyse der Property
$type = $property->getType();
$typeInfo = $this->analyzeType($type);
return new PropertyMetadata(
name: $propertyName,
columnName: '',
type: $typeInfo['mainType'],
nullable: $typeInfo['nullable'],
hasDefault: true,
defaultValue: null,
allTypes: $typeInfo['allTypes'],
primary: false,
autoIncrement: false,
isRelation: true,
relationTargetClass: $targetClass,
relationForeignKey: $foreignKey,
relationLocalKey: $localKey,
relationType: 'one-to-one'
);*/
}
public function determineRelationType(\ReflectionProperty $property): string
{
$propertyType = $property->getType();
if($propertyType instanceof \ReflectionNamedType && $propertyType->getName() === 'array') {
return 'hasMany';
}
$parentReflection = $property->getDeclaringClass();
$possibleForeignKeyProperty = $property->getName() . 'Id';
if($this->hasConstructorParameter($parentReflection, $possibleForeignKeyProperty)) {
return 'belongsTo';
}
$possibleForeignKeyProperty2 = $property->getName() . '_id';
if($this->hasConstructorParameter($parentReflection, $possibleForeignKeyProperty2)) {
return 'belongsTo';
}
return 'one-to-one';
}
private function hasConstructorParameter(ReflectionClass $reflection, string $paramName): bool
{
$constructor = $reflection->getConstructor();
if(!$constructor) {
return false;
}
foreach($constructor->getParameters() as $param) {
if($param->getName() === $paramName) {
return true;
}
}
return false;
}
private function createBelongsToRelationMetadata(\ReflectionProperty $property, string $targetClass, ReflectionClass $parentReflection): PropertyMetadata
{
$propertyName = $property->getName();
$foreignKeyProperty = $this->findForeignKeyProperty($propertyName, $parentReflection);
if(!$foreignKeyProperty) {
throw new DatabaseException("Could not find foreign key property for property {$propertyName} in class {$parentReflection->getName()}");
}
$foreignKeyColumnName = $this->getColumnName($foreignKeyProperty, $parentReflection);
$type = $property->getType();
$typeInfo = $this->analyzeType($type);
return new PropertyMetadata(
name: $propertyName,
columnName: '',
type: $typeInfo['mainType'],
nullable: $typeInfo['nullable'],
hasDefault: true,
defaultValue: null,
allTypes: $typeInfo['allTypes'],
primary: false,
autoIncrement: false,
isRelation: true,
relationTargetClass: $targetClass,
relationForeignKey: $foreignKeyColumnName, // Jetzt die Spalte, nicht der Parameter-Name
relationLocalKey: '', // Wird in belongsTo nicht verwendet
relationType: 'belongsTo'
);
}
private function findForeignKeyProperty(string $propertyName, ReflectionClass $parentReflection): ?string
{
$constructor = $parentReflection->getConstructor();
if (!$constructor) {
return null;
}
// Suche nach {property}Id
foreach ($constructor->getParameters() as $param) {
if ($param->getName() === $propertyName . 'Id') {
return $param->getName();
}
}
// Suche nach {property}_id (falls das der Parameter-Name ist)
foreach ($constructor->getParameters() as $param) {
if ($param->getName() === $propertyName . '_id') {
return $param->getName();
}
}
return null;
}
private function createHasRelationMetadata(\ReflectionProperty $property, string $targetClass, ReflectionClass $parentReflection, string $relationType): PropertyMetadata
{
$propertyName = $property->getName();
$targetReflection = new ReflectionClass($targetClass);
$targetEntityAttribute = $this->getEntityAttribute($targetReflection);
$foreignKey = $targetEntityAttribute?->idColumn ?? 'id';
$entityAttribute = $this->getEntityAttribute($parentReflection);
$localKey = $entityAttribute?->idColumn ?? 'id';
$type = $property->getType();
$typeInfo = $this->analyzeType($type);
return new PropertyMetadata(
name: $propertyName,
columnName: '',
type: $typeInfo['mainType'],
nullable: $typeInfo['nullable'],
hasDefault: true,
defaultValue: $relationType === 'hasMany' ? [] : null,
allTypes: $typeInfo['allTypes'],
primary: false,
autoIncrement: false,
isRelation: true,
relationTargetClass: $targetClass,
relationForeignKey: $foreignKey,
relationLocalKey: $localKey,
relationType: $relationType
);
}
private function extractRelationMetadata(ReflectionProperty $property, Type $typeAttribute, ReflectionClass $classReflection): PropertyMetadata
{
$propertyName = $property->getName();
// Ermittle foreign key automatisch falls nicht gesetzt
$foreignKey = $typeAttribute->foreignKey;
if (!$foreignKey) {
// Konvention: {parent_class}_id
$shortName = strtolower($classReflection->getShortName());
$foreignKey = $shortName . '_id';
}
// Ermittle local key automatisch falls nicht gesetzt
$localKey = $typeAttribute->localKey;
if (!$localKey) {
// Verwende ID-Property der Entity
$entityAttribute = $this->getEntityAttribute($classReflection);
$localKey = $entityAttribute?->idColumn ?? 'id';
}
// Typ-Analyse der Property
$type = $property->getType();
$typeInfo = $this->analyzeType($type);
return new PropertyMetadata(
name: $propertyName,
columnName: '', // Relationen haben keine direkte Spalte
type: $typeInfo['mainType'],
nullable: $typeInfo['nullable'],
hasDefault: true, // Relationen haben immer einen Default (leeres Array)
defaultValue: [],
allTypes: $typeInfo['allTypes'],
primary: false,
autoIncrement: false,
isRelation: true,
relationTargetClass: $typeAttribute->targetClass,
relationForeignKey: $foreignKey,
relationLocalKey: $localKey,
relationType: $typeAttribute->type
);
}
private function extractPropertyMetadata(ReflectionParameter $param, ReflectionClass $classReflection): PropertyMetadata
{
$paramName = $param->getName();
// Column-Name aus Attribute oder Parameter-Name
$columnName = $this->getColumnName($paramName, $classReflection);
// Typ-Analyse
$type = $param->getType();
$typeInfo = $this->analyzeType($type);
// Default-Werte und Nullable
$hasDefault = $param->isDefaultValueAvailable();
$defaultValue = $hasDefault ? $param->getDefaultValue() : null;
$nullable = $typeInfo['nullable'] || $hasDefault;
// Primary Key und Auto-Increment Eigenschaften aus Attribut auslesen
$columnAttribute = null;
$primary = false;
$autoIncrement = false;
try {
$property = $classReflection->getProperty($paramName);
$columnAttribute = $this->getColumnAttribute($property);
if ($columnAttribute) {
$primary = $columnAttribute->primary;
$autoIncrement = $columnAttribute->autoIncrement;
if ($columnAttribute->nullable) {
$nullable = true;
}
}
} catch (\ReflectionException) {
// Property nicht gefunden
}
return new PropertyMetadata(
name: $paramName,
columnName: $columnName,
type: $typeInfo['mainType'],
nullable: $nullable,
hasDefault: $hasDefault,
defaultValue: $defaultValue,
allTypes: $typeInfo['allTypes'],
primary: $primary,
autoIncrement: $autoIncrement
);
}
private function getColumnName(string $paramName, ReflectionClass $classReflection): string
{
try {
$property = $classReflection->getProperty($paramName);
$columnAttribute = $this->getColumnAttribute($property);
return $columnAttribute?->name ?? $paramName;
} catch (\ReflectionException) {
return $paramName;
}
}
private function analyzeType(?\ReflectionType $type): array
{
if (!$type) {
return [
'mainType' => 'mixed',
'allTypes' => ['mixed'],
'nullable' => true
];
}
if ($type instanceof ReflectionNamedType) {
return [
'mainType' => $type->getName(),
'allTypes' => [$type->getName()],
'nullable' => $type->allowsNull()
];
}
if ($type instanceof ReflectionUnionType) {
$types = [];
$nullable = false;
foreach ($type->getTypes() as $unionType) {
if ($unionType instanceof ReflectionNamedType) {
$typeName = $unionType->getName();
if ($typeName === 'null') {
$nullable = true;
} else {
$types[] = $typeName;
}
}
}
return [
'mainType' => $types[0] ?? 'mixed',
'allTypes' => $types,
'nullable' => $nullable
];
}
return [
'mainType' => 'mixed',
'allTypes' => ['mixed'],
'nullable' => true
];
}
private function getEntityAttribute(ReflectionClass $reflection): ?Entity
{
$attributes = $reflection->getAttributes(Entity::class);
return $attributes ? $attributes[0]->newInstance() : null;
}
private function getColumnAttribute(ReflectionProperty $property): ?Column
{
$attributes = $property->getAttributes(Column::class);
return $attributes ? $attributes[0]->newInstance() : null;
}
private function getTypeAttribute(ReflectionProperty $property): ?Type
{
$attributes = $property->getAttributes(Type::class);
return $attributes ? $attributes[0]->newInstance() : null;
}
private function getTableNameFromClass(string $className): string
{
$shortName = new ReflectionClass($className)->getShortName();
return strtolower($shortName) . 's';
}
/**
* Ermittelt den Property-Namen, der als ID dient
*/
private function findIdProperty(ReflectionClass $reflection, array $properties, string $idColumn): string
{
// 1. Suche nach Property mit primary=true im Column-Attribut
foreach ($properties as $propName => $propMetadata) {
try {
$property = $reflection->getProperty($propName);
$columnAttribute = $this->getColumnAttribute($property);
if ($columnAttribute && $columnAttribute->primary) {
return $propName;
}
} catch (\ReflectionException) {
// Property nicht gefunden, ignorieren
}
}
// 2. Suche nach Property, die dem idColumn entspricht
foreach ($properties as $propName => $propMetadata) {
if ($propMetadata->columnName === $idColumn) {
return $propName;
}
}
// 3. Fallback: Property mit Namen 'id'
if (isset($properties['id'])) {
return 'id';
}
// 4. Wenn nichts passt: Erste Property (nicht ideal, aber besser als nichts)
if (!empty($properties)) {
return array_key_first($properties);
}
throw new DatabaseException("Keine ID-Property für Entity {$reflection->getName()} gefunden");
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database\Metadata;
final class MetadataRegistry
{
private array $metadata = [];
public function __construct(
private readonly MetadataExtractor $extractor
) {}
public function getMetadata(string $entityClass): EntityMetadata
{
if (!isset($this->metadata[$entityClass])) {
$this->metadata[$entityClass] = $this->extractor->extractMetadata($entityClass);
}
return $this->metadata[$entityClass];
}
public function hasMetadata(string $entityClass): bool
{
return isset($this->metadata[$entityClass]);
}
public function clearCache(): void
{
$this->metadata = [];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Framework\Database\Metadata;
final readonly class PropertyMetadata
{
public function __construct(
public string $name,
public string $columnName,
public string $type,
public bool $nullable,
public bool $hasDefault,
public mixed $defaultValue,
public array $allTypes = [],
public bool $primary = false,
public bool $autoIncrement = false,
// Eigenschaften für Relationen
public bool $isRelation = false,
public ?string $relationTargetClass = null,
public ?string $relationForeignKey = null,
public ?string $relationLocalKey = null,
public string $relationType = 'hasMany'
) {}
public function isUnionType(): bool
{
return count($this->allTypes) > 1;
}
}