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

@@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
namespace App\Framework\Database;
use App\Framework\Database\Metadata\EntityMetadata;
use App\Framework\Database\Metadata\MetadataRegistry;
use App\Framework\Database\Metadata\PropertyMetadata;
final readonly class BatchRelationLoader
{
public function __construct(
private DatabaseManager $databaseManager,
private MetadataRegistry $metadataRegistry,
private IdentityMap $identityMap,
private HydratorInterface $hydrator
) {
}
/**
* Preload a specific relation for multiple entities in batches
*
* @param array<object> $entities
* @param string $relationName
*/
public function preloadRelation(array $entities, string $relationName): void
{
if (empty($entities)) {
return;
}
$entityClass = get_class($entities[0]);
$metadata = $this->metadataRegistry->getMetadata($entityClass);
$relationMetadata = $this->getRelationMetadata($metadata, $relationName);
if ($relationMetadata === null) {
return; // Relation not found
}
match ($relationMetadata->relationType) {
'hasMany' => $this->preloadHasMany($entities, $relationMetadata),
'belongsTo' => $this->preloadBelongsTo($entities, $relationMetadata),
'one-to-one' => $this->preloadOneToOne($entities, $relationMetadata),
default => null
};
}
/**
* Preload hasMany relations (1:N)
* Example: Post -> Comments
*/
private function preloadHasMany(array $entities, PropertyMetadata $relationMetadata): void
{
// Collect all local keys (e.g., post IDs)
$localKeys = array_filter(array_map(
fn ($entity) => $this->getLocalKey($entity, $relationMetadata),
$entities
));
if (empty($localKeys)) {
return;
}
// Load all related entities in one query
// WHERE foreign_key IN (1, 2, 3, ...)
$allRelations = $this->findByQuery(
$relationMetadata->relationTargetClass,
[$relationMetadata->relationForeignKey => $localKeys]
);
// Group relations by foreign key
$groupedRelations = $this->groupByForeignKey($allRelations, $relationMetadata->relationForeignKey);
// Set relations on entities
foreach ($entities as $entity) {
$localKey = $this->getLocalKey($entity, $relationMetadata);
$relations = $groupedRelations[$localKey] ?? [];
$this->setRelationOnEntity($entity, $relationMetadata->name, $relations);
}
}
/**
* Preload belongsTo relations (N:1)
* Example: Comment -> Post
*/
private function preloadBelongsTo(array $entities, PropertyMetadata $relationMetadata): void
{
// Collect all foreign keys (e.g., post IDs from comments)
$foreignKeys = array_filter(array_unique(array_map(
fn ($entity) => $this->getForeignKey($entity, $relationMetadata),
$entities
)));
if (empty($foreignKeys)) {
return;
}
// Load all related entities in one query
// WHERE id IN (1, 2, 3, ...)
$allRelations = $this->findByQuery(
$relationMetadata->relationTargetClass,
['id' => $foreignKeys] // Assuming 'id' is the primary key
);
// Index by primary key
$indexedRelations = $this->indexByPrimaryKey($allRelations);
// Set relations on entities
foreach ($entities as $entity) {
$foreignKey = $this->getForeignKey($entity, $relationMetadata);
$relation = $indexedRelations[$foreignKey] ?? null;
$this->setRelationOnEntity($entity, $relationMetadata->name, $relation);
}
}
/**
* Preload one-to-one relations
* Example: User -> Profile
*/
private function preloadOneToOne(array $entities, PropertyMetadata $relationMetadata): void
{
// Similar to belongsTo but expecting single result
$localKeys = array_filter(array_map(
fn ($entity) => $this->getLocalKey($entity, $relationMetadata),
$entities
));
if (empty($localKeys)) {
return;
}
$allRelations = $this->findByQuery(
$relationMetadata->relationTargetClass,
[$relationMetadata->relationForeignKey => $localKeys]
);
// Group by foreign key but expect only one result per key
$groupedRelations = $this->groupByForeignKey($allRelations, $relationMetadata->relationForeignKey);
foreach ($entities as $entity) {
$localKey = $this->getLocalKey($entity, $relationMetadata);
$relation = $groupedRelations[$localKey][0] ?? null; // Take first (should be only one)
$this->setRelationOnEntity($entity, $relationMetadata->name, $relation);
}
}
private function getRelationMetadata(EntityMetadata $metadata, string $relationName): ?PropertyMetadata
{
foreach ($metadata->properties as $property) {
if ($property->name === $relationName && $property->isRelation) {
return $property;
}
}
return null;
}
private function getLocalKey(object $entity, PropertyMetadata $relationMetadata): mixed
{
return $this->getPropertyValue($entity, $relationMetadata->relationLocalKey);
}
private function getForeignKey(object $entity, PropertyMetadata $relationMetadata): mixed
{
return $this->getPropertyValue($entity, $relationMetadata->relationForeignKey);
}
private function getPropertyValue(object $entity, string $propertyName): mixed
{
$reflection = new \ReflectionClass($entity);
// Try direct property access first
if ($reflection->hasProperty($propertyName)) {
$property = $reflection->getProperty($propertyName);
return $property->getValue($entity);
}
// Try getter method
$getterMethod = 'get' . ucfirst($propertyName);
if ($reflection->hasMethod($getterMethod)) {
return $entity->$getterMethod();
}
// Try ID property as fallback for primary keys
if ($propertyName === 'id' && $reflection->hasProperty('id')) {
$property = $reflection->getProperty('id');
return $property->getValue($entity);
}
return null;
}
/**
* Group entities by foreign key value
* @param array<object> $entities
* @param string $foreignKeyProperty
* @return array<mixed, array<object>>
*/
private function groupByForeignKey(array $entities, string $foreignKeyProperty): array
{
$grouped = [];
foreach ($entities as $entity) {
$key = $this->getPropertyValue($entity, $foreignKeyProperty);
if ($key !== null) {
$grouped[$key][] = $entity;
}
}
return $grouped;
}
/**
* Index entities by their primary key
* @param array<object> $entities
* @return array<mixed, object>
*/
private function indexByPrimaryKey(array $entities): array
{
$indexed = [];
foreach ($entities as $entity) {
$key = $this->getPropertyValue($entity, 'id'); // Assuming 'id' is primary key
if ($key !== null) {
$indexed[$key] = $entity;
}
}
return $indexed;
}
/**
* Internal method to execute database query and hydrate entities
* @param string $entityClass
* @param array $criteria
* @return array<object>
*/
private function findByQuery(string $entityClass, array $criteria): 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);
}
$result = $this->databaseManager->getConnection()->query($query, $params);
$entities = [];
foreach ($result->fetchAll() as $data) {
$idValue = $data[$metadata->idColumn];
// Check identity map first to avoid duplicates
if ($this->identityMap->has($entityClass, $idValue)) {
$entities[] = $this->identityMap->get($entityClass, $idValue);
} else {
$entity = $this->hydrator->hydrate($metadata, $data);
$this->identityMap->set($entityClass, $idValue, $entity);
$entities[] = $entity;
}
}
return $entities;
}
private function setRelationOnEntity(object $entity, string $relationName, mixed $relationValue): void
{
$reflection = new \ReflectionClass($entity);
// Try setter methods first
$setterMethod = 'set' . ucfirst($relationName);
if ($reflection->hasMethod($setterMethod)) {
$entity->$setterMethod($relationValue);
return;
}
// For readonly entities, we might need to use a different approach
// This is tricky with readonly properties - we might need to store in a cache
// For now, we'll skip setting on readonly entities
if ($reflection->hasProperty($relationName)) {
$property = $reflection->getProperty($relationName);
if (! $property->isReadOnly()) {
$property->setValue($entity, $relationValue);
}
}
}
}