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 */ 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(); } }