chore: complete update
This commit is contained in:
469
src/Framework/Database/EntityManager.php
Normal file
469
src/Framework/Database/EntityManager.php
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
namespace App\Framework\Database;
|
||||
|
||||
use App\Framework\Attributes\Singleton;
|
||||
use App\Framework\Database\Metadata\EntityMetadata;
|
||||
use App\Framework\Database\Metadata\MetadataRegistry;
|
||||
|
||||
#[Singleton]
|
||||
final readonly class EntityManager
|
||||
{
|
||||
private Hydrator $hydrator;
|
||||
public function __construct(
|
||||
private DatabaseManager $databaseManager,
|
||||
private MetadataRegistry $metadataRegistry,
|
||||
private TypeConverter $typeConverter,
|
||||
private IdentityMap $identityMap,
|
||||
private LazyLoader $lazyLoader
|
||||
) {
|
||||
|
||||
$this->hydrator = new Hydrator($this->typeConverter, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Entity - standardmäßig lazy loading
|
||||
*/
|
||||
public function find(string $entityClass, mixed $id): ?object
|
||||
{
|
||||
// Prüfe Identity Map zuerst
|
||||
if ($this->identityMap->has($entityClass, $id)) {
|
||||
return $this->identityMap->get($entityClass, $id);
|
||||
}
|
||||
|
||||
return $this->findWithLazyLoading($entityClass, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Entity und lädt sie sofort (eager loading)
|
||||
*/
|
||||
public function findEager(string $entityClass, mixed $id): ?object
|
||||
{
|
||||
// Prüfe Identity Map zuerst
|
||||
if ($this->identityMap->has($entityClass, $id)) {
|
||||
$entity = $this->identityMap->get($entityClass, $id);
|
||||
|
||||
// Falls es ein Ghost ist, initialisiere es
|
||||
if ($this->isLazyGhost($entity)) {
|
||||
$this->initializeLazyObject($entity);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
|
||||
$query = "SELECT * FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
|
||||
$result = $this->databaseManager->getConnection()->query($query, [$id]);
|
||||
$data = $result->fetch();
|
||||
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = $this->hydrator->hydrate($metadata, $data);
|
||||
|
||||
// In Identity Map speichern
|
||||
$this->identityMap->set($entityClass, $id, $entity);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interne Methode für Lazy Loading
|
||||
*/
|
||||
private function findWithLazyLoading(string $entityClass, mixed $id): ?object
|
||||
{
|
||||
// Prüfe ob Entity existiert (schneller Check)
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
|
||||
$query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
|
||||
$result = $this->databaseManager->getConnection()->query($query, [$id]);
|
||||
|
||||
if (!$result->fetch()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Erstelle Lazy Ghost
|
||||
return $this->lazyLoader->createLazyGhost($metadata, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Referenz auf Entity (ohne Existenz-Check)
|
||||
*/
|
||||
public function getReference(string $entityClass, mixed $id): object
|
||||
{
|
||||
// Prüfe Identity Map
|
||||
if ($this->identityMap->has($entityClass, $id)) {
|
||||
return $this->identityMap->get($entityClass, $id);
|
||||
}
|
||||
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
return $this->lazyLoader->createLazyGhost($metadata, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet alle Entities - standardmäßig lazy
|
||||
*/
|
||||
public function findAll(string $entityClass): array
|
||||
{
|
||||
return $this->findAllLazy($entityClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet alle Entities und lädt sie sofort
|
||||
*/
|
||||
public function findAllEager(string $entityClass): array
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
|
||||
$query = "SELECT * FROM {$metadata->tableName}";
|
||||
$result = $this->databaseManager->getConnection()->query($query);
|
||||
|
||||
$entities = [];
|
||||
foreach ($result->fetchAll() as $data) {
|
||||
// Prüfe Identity Map für jede Entity
|
||||
$idValue = $data[$metadata->idColumn];
|
||||
|
||||
if ($this->identityMap->has($entityClass, $idValue)) {
|
||||
$entity = $this->identityMap->get($entityClass, $idValue);
|
||||
|
||||
// Falls Ghost, initialisiere es für eager loading
|
||||
if ($this->isLazyGhost($entity)) {
|
||||
$this->initializeLazyObject($entity);
|
||||
}
|
||||
|
||||
$entities[] = $entity;
|
||||
} else {
|
||||
$entity = $this->hydrator->hydrate($metadata, $data);
|
||||
$this->identityMap->set($entityClass, $idValue, $entity);
|
||||
$entities[] = $entity;
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interne Methode für lazy findAll
|
||||
*/
|
||||
private function findAllLazy(string $entityClass): array
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
|
||||
// Nur IDs laden
|
||||
$query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName}";
|
||||
$result = $this->databaseManager->getConnection()->query($query);
|
||||
|
||||
$entities = [];
|
||||
foreach ($result->fetchAll() as $data) {
|
||||
$idValue = $data[$metadata->idColumn];
|
||||
|
||||
if ($this->identityMap->has($entityClass, $idValue)) {
|
||||
$entities[] = $this->identityMap->get($entityClass, $idValue);
|
||||
} else {
|
||||
$entities[] = $this->lazyLoader->createLazyGhost($metadata, $idValue);
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet Entities nach Kriterien
|
||||
*/
|
||||
public function findBy(string $entityClass, array $criteria, ?array $orderBy = null, ?int $limit = null): array
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
|
||||
$query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName}";
|
||||
$params = [];
|
||||
|
||||
if (!empty($criteria)) {
|
||||
$conditions = [];
|
||||
foreach ($criteria as $field => $value) {
|
||||
$columnName = $metadata->getColumnName($field);
|
||||
$conditions[] = "{$columnName} = ?";
|
||||
$params[] = $value;
|
||||
}
|
||||
$query .= " WHERE " . implode(' AND ', $conditions);
|
||||
}
|
||||
|
||||
if ($orderBy) {
|
||||
$orderClauses = [];
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$columnName = $metadata->getColumnName($field);
|
||||
$orderClauses[] = "{$columnName} " . strtoupper($direction);
|
||||
}
|
||||
$query .= " ORDER BY " . implode(', ', $orderClauses);
|
||||
}
|
||||
|
||||
if ($limit) {
|
||||
$query .= " LIMIT {$limit}";
|
||||
}
|
||||
|
||||
$result = $this->databaseManager->getConnection()->query($query, $params);
|
||||
|
||||
$entities = [];
|
||||
foreach ($result->fetchAll() as $data) {
|
||||
$idValue = $data[$metadata->idColumn];
|
||||
$entities[] = $this->find($entityClass, $idValue); // Nutzt lazy loading
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet eine Entity nach Kriterien
|
||||
*/
|
||||
public function findOneBy(string $entityClass, array $criteria): ?object
|
||||
{
|
||||
$results = $this->findBy($entityClass, $criteria, limit: 1);
|
||||
return $results[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility Methods
|
||||
*/
|
||||
public function detach(object $entity): void
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entity::class);
|
||||
|
||||
// ID der Entity ermitteln
|
||||
$constructor = $metadata->reflection->getConstructor();
|
||||
if ($constructor) {
|
||||
foreach ($constructor->getParameters() as $param) {
|
||||
$paramName = $param->getName();
|
||||
$propertyMetadata = $metadata->getProperty($paramName);
|
||||
|
||||
if ($propertyMetadata && $propertyMetadata->columnName === $metadata->idColumn) {
|
||||
try {
|
||||
$property = $metadata->reflection->getProperty($paramName);
|
||||
$id = $property->getValue($entity);
|
||||
$this->identityMap->remove($entity::class, $id);
|
||||
break;
|
||||
} catch (\ReflectionException) {
|
||||
// Property nicht gefunden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->identityMap->clear();
|
||||
}
|
||||
|
||||
public function getIdentityMapStats(): array
|
||||
{
|
||||
return $this->identityMap->getStats();
|
||||
}
|
||||
|
||||
public function isLazyGhost(object $entity): bool
|
||||
{
|
||||
return $this->lazyLoader->isLazyGhost($entity);
|
||||
}
|
||||
|
||||
public function initializeLazyObject(object $entity): void
|
||||
{
|
||||
$this->lazyLoader->initializeLazyObject($entity);
|
||||
}
|
||||
|
||||
public function getMetadata(string $entityClass): EntityMetadata
|
||||
{
|
||||
return $this->metadataRegistry->getMetadata($entityClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine einzigartige ID für Entities
|
||||
*/
|
||||
public function generateId(): string
|
||||
{
|
||||
return IdGenerator::generate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert eine Entity (INSERT oder UPDATE)
|
||||
*/
|
||||
public function save(object $entity): object
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entity::class);
|
||||
|
||||
$idProperty = $metadata->reflection->getProperty($metadata->idProperty);
|
||||
$id = $idProperty->getValue($entity);
|
||||
|
||||
// Prüfe ob Entity bereits existiert
|
||||
if ($this->exists($entity::class, $id)) {
|
||||
return $this->update($entity);
|
||||
} else {
|
||||
return $this->insert($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Entity mit der angegebenen ID existiert
|
||||
*/
|
||||
public function exists(string $entityClass, mixed $id): bool
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
||||
|
||||
$query = "SELECT 1 FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ? LIMIT 1";
|
||||
$result = $this->databaseManager->getConnection()->query($query, [$id]);
|
||||
|
||||
return (bool) $result->fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine neue Entity ein (INSERT)
|
||||
*/
|
||||
public function insert(object $entity): object
|
||||
{
|
||||
|
||||
|
||||
$metadata = $this->metadataRegistry->getMetadata($entity::class);
|
||||
|
||||
// Columnnamen und Values sammeln
|
||||
$columns = [];
|
||||
$values = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($metadata->properties as $propertyName => $propertyMetadata) {
|
||||
// ID Property überspringen wenn auto-increment
|
||||
if ($propertyName === $metadata->idProperty && $propertyMetadata->autoIncrement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Property-Wert auslesen
|
||||
$property = $metadata->reflection->getProperty($propertyName);
|
||||
|
||||
if(!$property->isInitialized($entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$columns[] = $propertyMetadata->columnName;
|
||||
$values[] = '?';
|
||||
|
||||
$params[] = $property->getValue($entity);
|
||||
}
|
||||
|
||||
// INSERT Query bauen
|
||||
$query = "INSERT INTO {$metadata->tableName} (" . implode(', ', $columns) . ") "
|
||||
. "VALUES (" . implode(', ', $values) . ")";
|
||||
|
||||
// Query ausführen
|
||||
$result = $this->databaseManager->getConnection()->execute($query, $params);
|
||||
|
||||
// In Identity Map speichern
|
||||
$idProperty = $metadata->reflection->getProperty($metadata->idProperty);
|
||||
$id = $idProperty->getValue($entity);
|
||||
$this->identityMap->set($entity::class, $id, $entity);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert eine vorhandene Entity (UPDATE)
|
||||
*/
|
||||
public function update(object $entity): object
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entity::class);
|
||||
|
||||
// SET-Clause und Params aufbauen
|
||||
$setClause = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($metadata->properties as $propertyName => $propertyMetadata) {
|
||||
|
||||
// Relations beim Update ignorieren
|
||||
if($propertyMetadata->isRelation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ID überspringen (wird nicht aktualisiert)
|
||||
if ($propertyName === $metadata->idProperty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$setClause[] = "{$propertyMetadata->columnName} = ?";
|
||||
|
||||
// Property-Wert auslesen
|
||||
$property = $metadata->reflection->getProperty($propertyName);
|
||||
$params[] = $property->getValue($entity);
|
||||
}
|
||||
|
||||
// ID für WHERE Clause hinzufügen
|
||||
$idProperty = $metadata->reflection->getProperty($metadata->idProperty);
|
||||
$id = $idProperty->getValue($entity);
|
||||
$params[] = $id;
|
||||
|
||||
// UPDATE Query bauen
|
||||
$query = "UPDATE {$metadata->tableName} SET " . implode(', ', $setClause)
|
||||
. " WHERE {$metadata->idColumn} = ?";
|
||||
|
||||
// Query ausführen
|
||||
$result = $this->databaseManager->getConnection()->query($query, $params);
|
||||
|
||||
// Identity Map aktualisieren
|
||||
$this->identityMap->set($entity::class, $id, $entity);
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Entity
|
||||
*/
|
||||
public function delete(object $entity): void
|
||||
{
|
||||
$metadata = $this->metadataRegistry->getMetadata($entity::class);
|
||||
|
||||
// ID auslesen
|
||||
$idProperty = $metadata->reflection->getProperty($metadata->idProperty);
|
||||
$id = $idProperty->getValue($entity);
|
||||
|
||||
// DELETE Query ausführen
|
||||
$query = "DELETE FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
|
||||
$this->databaseManager->getConnection()->query($query, [$id]);
|
||||
|
||||
// Aus Identity Map entfernen
|
||||
$this->identityMap->remove($entity::class, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert mehrere Entities auf einmal
|
||||
*/
|
||||
public function saveAll(object ...$entities): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($entities as $entity) {
|
||||
$result[] = $this->save($entity);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt eine Funktion in einer Transaktion aus
|
||||
*/
|
||||
public function transaction(callable $callback): mixed
|
||||
{
|
||||
$connection = $this->databaseManager->getConnection();
|
||||
|
||||
// Wenn bereits in einer Transaktion, führe Callback direkt aus
|
||||
if ($connection->inTransaction()) {
|
||||
return $callback($this);
|
||||
}
|
||||
|
||||
// Neue Transaktion starten
|
||||
$connection->beginTransaction();
|
||||
|
||||
try {
|
||||
$result = $callback($this);
|
||||
$connection->commit();
|
||||
return $result;
|
||||
} catch (\Throwable $e) {
|
||||
$connection->rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user