- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
890 lines
28 KiB
PHP
890 lines
28 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Database;
|
|
|
|
use App\Framework\Attributes\Singleton;
|
|
use App\Framework\Database\Cache\EntityCacheManager;
|
|
use App\Framework\Database\Criteria\Criteria;
|
|
use App\Framework\Database\Criteria\CriteriaQuery;
|
|
use App\Framework\Database\Events\EntityEventManager;
|
|
use App\Framework\Database\Metadata\EntityMetadata;
|
|
use App\Framework\Database\Metadata\MetadataRegistry;
|
|
use App\Framework\Database\QueryBuilder\QueryBuilderFactory;
|
|
use App\Framework\Database\QueryBuilder\SelectQueryBuilder;
|
|
use App\Framework\Database\UnitOfWork\UnitOfWork;
|
|
|
|
#[Singleton]
|
|
final readonly class EntityManager implements EntityLoaderInterface
|
|
{
|
|
public function __construct(
|
|
private DatabaseManager $databaseManager,
|
|
private MetadataRegistry $metadataRegistry,
|
|
private TypeConverter $typeConverter,
|
|
private IdentityMap $identityMap,
|
|
private LazyLoader $lazyLoader,
|
|
private HydratorInterface $hydrator,
|
|
private BatchRelationLoader $batchRelationLoader,
|
|
public UnitOfWork $unitOfWork,
|
|
private QueryBuilderFactory $queryBuilderFactory,
|
|
private EntityEventManager $entityEventManager,
|
|
private ?EntityCacheManager $cacheManager = null
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Findet Entity - standardmäßig lazy loading
|
|
*/
|
|
public function find(string $entityClass, mixed $id): ?object
|
|
{
|
|
// Use cache manager if available
|
|
if ($this->cacheManager !== null) {
|
|
return $this->cacheManager->findEntity($entityClass, $id, function () use ($entityClass, $id) {
|
|
return $this->findWithLazyLoading($entityClass, $id);
|
|
});
|
|
}
|
|
|
|
// Fallback to direct loading
|
|
// 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);
|
|
|
|
// Entity Loaded Event dispatchen
|
|
$this->entityEventManager->entityLoaded($entity, $entityClass, $id, $data, false);
|
|
|
|
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
|
|
$entity = $this->lazyLoader->createLazyGhost($metadata, $id);
|
|
|
|
// Entity Loaded Event dispatchen (als Lazy)
|
|
$this->entityEventManager->entityLoaded($entity, $entityClass, $id, [], true);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
{
|
|
// Use cache manager if available
|
|
if ($this->cacheManager !== null) {
|
|
return $this->cacheManager->findCollection($entityClass, $criteria, $orderBy, $limit, null, function () use ($entityClass, $criteria, $orderBy, $limit) {
|
|
return $this->findByWithoutCache($entityClass, $criteria, $orderBy, $limit);
|
|
});
|
|
}
|
|
|
|
return $this->findByWithoutCache($entityClass, $criteria, $orderBy, $limit);
|
|
}
|
|
|
|
/**
|
|
* Internal method for finding entities without cache
|
|
*/
|
|
private function findByWithoutCache(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;
|
|
}
|
|
|
|
/**
|
|
* Findet Entities mit vorab geladenen Relationen (N+1 Solution)
|
|
*
|
|
* @param string $entityClass
|
|
* @param array $criteria
|
|
* @param array $relations Relations to preload ['comments', 'author', etc.]
|
|
* @param array|null $orderBy
|
|
* @param int|null $limit
|
|
* @return array<object>
|
|
*/
|
|
public function findWithRelations(
|
|
string $entityClass,
|
|
array $criteria = [],
|
|
array $relations = [],
|
|
?array $orderBy = null,
|
|
?int $limit = null
|
|
): array {
|
|
// Step 1: Load base entities (using existing findBy logic but eager)
|
|
$entities = $this->findByEager($entityClass, $criteria, $orderBy, $limit);
|
|
|
|
if (empty($entities) || empty($relations)) {
|
|
return $entities;
|
|
}
|
|
|
|
// Step 2: Preload each specified relation in batch
|
|
foreach ($relations as $relationName) {
|
|
$this->batchRelationLoader->preloadRelation($entities, $relationName);
|
|
}
|
|
|
|
return $entities;
|
|
}
|
|
|
|
/**
|
|
* Eager version of findBy - loads full entities immediately
|
|
* Used internally by findWithRelations
|
|
*/
|
|
private function findByEager(string $entityClass, array $criteria, ?array $orderBy = null, ?int $limit = null): array
|
|
{
|
|
$metadata = $this->metadataRegistry->getMetadata($entityClass);
|
|
|
|
$query = "SELECT * FROM {$metadata->tableName}";
|
|
$params = [];
|
|
|
|
if (! empty($criteria)) {
|
|
$conditions = [];
|
|
foreach ($criteria as $field => $value) {
|
|
if (is_array($value)) {
|
|
// Handle IN queries for batch loading
|
|
$placeholders = str_repeat('?,', count($value) - 1) . '?';
|
|
$columnName = $metadata->getColumnName($field);
|
|
$conditions[] = "{$columnName} IN ({$placeholders})";
|
|
$params = array_merge($params, array_values($value));
|
|
} else {
|
|
$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];
|
|
|
|
// Check identity map first
|
|
if ($this->identityMap->has($entityClass, $idValue)) {
|
|
$entity = $this->identityMap->get($entityClass, $idValue);
|
|
|
|
// If it's a lazy ghost, initialize it for 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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Entity Detached Event dispatchen (vor Identity Map Remove)
|
|
$this->entityEventManager->entityDetached($entity, $entity::class, $id);
|
|
|
|
$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();
|
|
}
|
|
|
|
/**
|
|
* Get profiling statistics if profiling is enabled
|
|
*/
|
|
public function getProfilingStatistics(): ?array
|
|
{
|
|
$connection = $this->databaseManager->getConnection();
|
|
|
|
if ($connection instanceof \App\Framework\Database\Profiling\ProfilingConnection) {
|
|
return $connection->getProfilingStatistics();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get profiling summary if profiling is enabled
|
|
*/
|
|
public function getProfilingSummary(): ?\App\Framework\Database\Profiling\ProfileSummary
|
|
{
|
|
$connection = $this->databaseManager->getConnection();
|
|
|
|
if ($connection instanceof \App\Framework\Database\Profiling\ProfilingConnection) {
|
|
return $connection->getProfilingSummary();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Clear profiling data if profiling is enabled
|
|
*/
|
|
public function clearProfilingData(): void
|
|
{
|
|
$connection = $this->databaseManager->getConnection();
|
|
|
|
if ($connection instanceof \App\Framework\Database\Profiling\ProfilingConnection) {
|
|
$connection->clearProfilingData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable or disable profiling at runtime
|
|
*/
|
|
public function setProfilingEnabled(bool $enabled): void
|
|
{
|
|
$connection = $this->databaseManager->getConnection();
|
|
|
|
if ($connection instanceof \App\Framework\Database\Profiling\ProfilingConnection) {
|
|
$connection->setProfilingEnabled($enabled);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if profiling is enabled
|
|
*/
|
|
public function isProfilingEnabled(): bool
|
|
{
|
|
$connection = $this->databaseManager->getConnection();
|
|
|
|
if ($connection instanceof \App\Framework\Database\Profiling\ProfilingConnection) {
|
|
return $connection->isProfilingEnabled();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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)) {
|
|
$result = $this->update($entity);
|
|
} else {
|
|
$result = $this->insert($entity);
|
|
}
|
|
|
|
// Cache the entity after successful save
|
|
if ($this->cacheManager !== null) {
|
|
$this->cacheManager->cacheEntity($result);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
// Entity Created Event dispatchen
|
|
$this->entityEventManager->entityCreated($entity, $entity::class, $id, $params);
|
|
|
|
return $entity;
|
|
}
|
|
|
|
/**
|
|
* Aktualisiert eine vorhandene Entity (UPDATE)
|
|
*/
|
|
public function update(object $entity): object
|
|
{
|
|
$metadata = $this->metadataRegistry->getMetadata($entity::class);
|
|
|
|
// ID für Change Tracking und WHERE Clause
|
|
$idProperty = $metadata->reflection->getProperty($metadata->idProperty);
|
|
$id = $idProperty->getValue($entity);
|
|
|
|
// Change Tracking: Original Entity aus IdentityMap laden
|
|
$originalEntity = $this->identityMap->get($entity::class, $id);
|
|
$changes = [];
|
|
$oldValues = [];
|
|
$newValues = [];
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Property-Wert auslesen
|
|
$property = $metadata->reflection->getProperty($propertyName);
|
|
$newValue = $property->getValue($entity);
|
|
|
|
// Change Tracking: Vergleiche mit Original-Werten
|
|
$oldValue = null;
|
|
if ($originalEntity !== null) {
|
|
$originalProperty = $metadata->reflection->getProperty($propertyName);
|
|
$oldValue = $originalProperty->getValue($originalEntity);
|
|
}
|
|
|
|
// Nur bei Änderungen in SET clause aufnehmen und Changes tracken
|
|
if ($originalEntity === null || $oldValue !== $newValue) {
|
|
$setClause[] = "{$propertyMetadata->columnName} = ?";
|
|
$params[] = $newValue;
|
|
|
|
// Change Tracking Daten sammeln
|
|
$changes[] = $propertyName;
|
|
$oldValues[$propertyName] = $oldValue;
|
|
$newValues[$propertyName] = $newValue;
|
|
}
|
|
}
|
|
|
|
// Wenn keine Änderungen vorliegen, kein UPDATE ausführen
|
|
if (empty($setClause)) {
|
|
return $entity;
|
|
}
|
|
|
|
// ID für WHERE Clause hinzufügen
|
|
$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);
|
|
|
|
// Entity Updated Event mit Change Tracking dispatchen
|
|
$this->entityEventManager->entityUpdated($entity, $entity::class, $id, $changes, $oldValues, $newValues);
|
|
|
|
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]);
|
|
|
|
// Evict from cache
|
|
if ($this->cacheManager !== null) {
|
|
$this->cacheManager->evictEntity($entity);
|
|
}
|
|
|
|
// Entity Deleted Event dispatchen (vor Identity Map Remove)
|
|
$this->entityEventManager->entityDeleted($entity, $entity::class, $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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute criteria query and return entities
|
|
*/
|
|
public function findByCriteria(Criteria $criteria): array
|
|
{
|
|
$metadata = $this->metadataRegistry->getMetadata($criteria->entityClass);
|
|
$criteriaQuery = new CriteriaQuery($criteria, $metadata->tableName);
|
|
|
|
$sql = $criteriaQuery->toSql();
|
|
$parameters = $criteriaQuery->getParameters();
|
|
|
|
$result = $this->databaseManager->getConnection()->query($sql, $parameters);
|
|
|
|
$entities = [];
|
|
foreach ($result->fetchAll() as $data) {
|
|
// Check if it's a projection query (non-entity result)
|
|
$projection = $criteria->getProjection();
|
|
if ($projection && count($projection->getAliases()) > 0) {
|
|
// Return raw data for projection queries
|
|
$entities[] = $data;
|
|
} else {
|
|
// Normal entity hydration
|
|
$idValue = $data[$metadata->idColumn];
|
|
|
|
if ($this->identityMap->has($criteria->entityClass, $idValue)) {
|
|
$entities[] = $this->identityMap->get($criteria->entityClass, $idValue);
|
|
} else {
|
|
$entity = $this->hydrator->hydrate($metadata, $data);
|
|
$this->identityMap->set($criteria->entityClass, $idValue, $entity);
|
|
$entities[] = $entity;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $entities;
|
|
}
|
|
|
|
/**
|
|
* Execute criteria query and return first result
|
|
*/
|
|
public function findOneByCriteria(Criteria $criteria): mixed
|
|
{
|
|
$criteria->setMaxResults(1);
|
|
$results = $this->findByCriteria($criteria);
|
|
|
|
return $results[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Count entities matching criteria
|
|
*/
|
|
public function countByCriteria(Criteria $criteria): int
|
|
{
|
|
$metadata = $this->metadataRegistry->getMetadata($criteria->entityClass);
|
|
|
|
// Create count criteria
|
|
$countCriteria = clone $criteria;
|
|
$countCriteria->setProjection(\App\Framework\Database\Criteria\Projections::count());
|
|
$countCriteria->setMaxResults(null);
|
|
$countCriteria->setFirstResult(null);
|
|
|
|
$criteriaQuery = new CriteriaQuery($countCriteria, $metadata->tableName);
|
|
|
|
$sql = $criteriaQuery->toSql();
|
|
$parameters = $criteriaQuery->getParameters();
|
|
|
|
$result = $this->databaseManager->getConnection()->queryScalar($sql, $parameters);
|
|
|
|
return (int) $result;
|
|
}
|
|
|
|
/**
|
|
* Create a new query builder
|
|
*/
|
|
public function createQueryBuilder(): SelectQueryBuilder
|
|
{
|
|
return $this->queryBuilderFactory->select();
|
|
}
|
|
|
|
/**
|
|
* Create a query builder for a specific entity
|
|
*/
|
|
public function createQueryBuilderFor(string $entityClass): SelectQueryBuilder
|
|
{
|
|
return $this->queryBuilderFactory->selectFromWithEntity($entityClass, $this->identityMap, $this->hydrator);
|
|
}
|
|
|
|
/**
|
|
* Create a query builder for a table
|
|
*/
|
|
public function createQueryBuilderForTable(string $tableName): SelectQueryBuilder
|
|
{
|
|
return $this->queryBuilderFactory->selectFromTable($tableName);
|
|
}
|
|
|
|
/**
|
|
* Get the identity map (for QueryBuilder integration)
|
|
*/
|
|
public function getIdentityMap(): IdentityMap
|
|
{
|
|
return $this->identityMap;
|
|
}
|
|
|
|
/**
|
|
* Get the hydrator (for QueryBuilder integration)
|
|
*/
|
|
public function getHydrator(): HydratorInterface
|
|
{
|
|
return $this->hydrator;
|
|
}
|
|
|
|
/**
|
|
* Get the entity event manager
|
|
*/
|
|
public function getEntityEventManager(): EntityEventManager
|
|
{
|
|
return $this->entityEventManager;
|
|
}
|
|
|
|
/**
|
|
* Record a domain event for an entity
|
|
*/
|
|
public function recordDomainEvent(object $entity, object $event): void
|
|
{
|
|
$this->entityEventManager->recordDomainEvent($entity, $event);
|
|
}
|
|
|
|
/**
|
|
* Dispatch all domain events for an entity
|
|
*/
|
|
public function dispatchDomainEventsForEntity(object $entity): void
|
|
{
|
|
$this->entityEventManager->dispatchDomainEventsForEntity($entity);
|
|
}
|
|
|
|
/**
|
|
* Dispatch all domain events across all entities
|
|
*/
|
|
public function dispatchAllDomainEvents(): void
|
|
{
|
|
$this->entityEventManager->dispatchAllDomainEvents();
|
|
}
|
|
|
|
/**
|
|
* Get domain event statistics
|
|
*/
|
|
public function getDomainEventStats(): array
|
|
{
|
|
return $this->entityEventManager->getDomainEventStats();
|
|
}
|
|
}
|