Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -1,24 +1,36 @@
<?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
final readonly class EntityManager implements EntityLoaderInterface
{
private Hydrator $hydrator;
public function __construct(
private DatabaseManager $databaseManager,
private MetadataRegistry $metadataRegistry,
private TypeConverter $typeConverter,
private IdentityMap $identityMap,
private LazyLoader $lazyLoader
private LazyLoader $lazyLoader,
private HydratorInterface $hydrator,
private BatchRelationLoader $batchRelationLoader,
public UnitOfWork $unitOfWork,
private QueryBuilderFactory $queryBuilderFactory,
private EntityEventManager $entityEventManager,
private ?EntityCacheManager $cacheManager = null
) {
$this->hydrator = new Hydrator($this->typeConverter, $this);
}
/**
@@ -26,6 +38,14 @@ final readonly class EntityManager
*/
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);
@@ -57,7 +77,7 @@ final readonly class EntityManager
$result = $this->databaseManager->getConnection()->query($query, [$id]);
$data = $result->fetch();
if (!$data) {
if (! $data) {
return null;
}
@@ -66,6 +86,9 @@ final readonly class EntityManager
// In Identity Map speichern
$this->identityMap->set($entityClass, $id, $entity);
// Entity Loaded Event dispatchen
$this->entityEventManager->entityLoaded($entity, $entityClass, $id, $data, false);
return $entity;
}
@@ -80,12 +103,17 @@ final readonly class EntityManager
$query = "SELECT {$metadata->idColumn} FROM {$metadata->tableName} WHERE {$metadata->idColumn} = ?";
$result = $this->databaseManager->getConnection()->query($query, [$id]);
if (!$result->fetch()) {
if (! $result->fetch()) {
return null;
}
// Erstelle Lazy Ghost
return $this->lazyLoader->createLazyGhost($metadata, $id);
$entity = $this->lazyLoader->createLazyGhost($metadata, $id);
// Entity Loaded Event dispatchen (als Lazy)
$this->entityEventManager->entityLoaded($entity, $entityClass, $id, [], true);
return $entity;
}
/**
@@ -99,6 +127,7 @@ final readonly class EntityManager
}
$metadata = $this->metadataRegistry->getMetadata($entityClass);
return $this->lazyLoader->createLazyGhost($metadata, $id);
}
@@ -173,13 +202,28 @@ final readonly class EntityManager
* 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)) {
if (! empty($criteria)) {
$conditions = [];
foreach ($criteria as $field => $value) {
$columnName = $metadata->getColumnName($field);
@@ -219,9 +263,110 @@ final readonly class EntityManager
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
*/
@@ -240,7 +385,12 @@ final readonly class EntityManager
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
@@ -283,6 +433,72 @@ final readonly class EntityManager
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)
*/
@@ -295,10 +511,17 @@ final readonly class EntityManager
// Prüfe ob Entity bereits existiert
if ($this->exists($entity::class, $id)) {
return $this->update($entity);
$result = $this->update($entity);
} else {
return $this->insert($entity);
$result = $this->insert($entity);
}
// Cache the entity after successful save
if ($this->cacheManager !== null) {
$this->cacheManager->cacheEntity($result);
}
return $result;
}
/**
@@ -337,7 +560,7 @@ final readonly class EntityManager
// Property-Wert auslesen
$property = $metadata->reflection->getProperty($propertyName);
if(!$property->isInitialized($entity)) {
if (! $property->isInitialized($entity)) {
continue;
}
@@ -360,6 +583,9 @@ final readonly class EntityManager
$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;
}
@@ -370,6 +596,16 @@ final readonly class EntityManager
{
$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 = [];
@@ -377,7 +613,7 @@ final readonly class EntityManager
foreach ($metadata->properties as $propertyName => $propertyMetadata) {
// Relations beim Update ignorieren
if($propertyMetadata->isRelation) {
if ($propertyMetadata->isRelation) {
continue;
}
@@ -386,16 +622,35 @@ final readonly class EntityManager
continue;
}
$setClause[] = "{$propertyMetadata->columnName} = ?";
// Property-Wert auslesen
$property = $metadata->reflection->getProperty($propertyName);
$params[] = $property->getValue($entity);
$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
$idProperty = $metadata->reflection->getProperty($metadata->idProperty);
$id = $idProperty->getValue($entity);
$params[] = $id;
// UPDATE Query bauen
@@ -408,6 +663,9 @@ final readonly class EntityManager
// 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;
}
@@ -426,6 +684,14 @@ final readonly class EntityManager
$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);
}
@@ -439,6 +705,7 @@ final readonly class EntityManager
foreach ($entities as $entity) {
$result[] = $this->save($entity);
}
return $result;
}
@@ -460,10 +727,163 @@ final readonly class EntityManager
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();
}
}