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:
305
src/Framework/Database/BatchRelationLoader.php
Normal file
305
src/Framework/Database/BatchRelationLoader.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user