chore: complete update
This commit is contained in:
30
src/Framework/Database/Metadata/EntityMetadata.php
Normal file
30
src/Framework/Database/Metadata/EntityMetadata.php
Normal 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;
|
||||
}
|
||||
}
|
||||
501
src/Framework/Database/Metadata/MetadataExtractor.php
Normal file
501
src/Framework/Database/Metadata/MetadataExtractor.php
Normal 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");
|
||||
}
|
||||
}
|
||||
33
src/Framework/Database/Metadata/MetadataRegistry.php
Normal file
33
src/Framework/Database/Metadata/MetadataRegistry.php
Normal 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 = [];
|
||||
}
|
||||
}
|
||||
29
src/Framework/Database/Metadata/PropertyMetadata.php
Normal file
29
src/Framework/Database/Metadata/PropertyMetadata.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user