['id' => 1, 'name' => 'Alice', 'teamId' => 10], 2 => ['id' => 2, 'name' => 'Bob', 'teamId' => 10], 3 => ['id' => 3, 'name' => 'Charlie', 'teamId' => 20], 4 => ['id' => 4, 'name' => 'Diana', 'teamId' => 20], ]; private array $teams = [ 10 => ['id' => 10, 'name' => 'Engineering'], 20 => ['id' => 20, 'name' => 'Design'], ]; public function findUser(int $id): ?array { $this->queryCount++; echo " šŸ“Š Query #{$this->queryCount}: findUser({$id})\n"; return $this->users[$id] ?? null; } public function findUsersByIds(array $ids): array { $this->queryCount++; echo " šŸ“Š Query #{$this->queryCount}: findUsersByIds([" . implode(', ', $ids) . "])\n"; $result = []; foreach ($ids as $id) { if (isset($this->users[$id])) { $result[$id] = $this->users[$id]; } } return $result; } public function findTeam(int $id): ?array { $this->queryCount++; echo " šŸ“Š Query #{$this->queryCount}: findTeam({$id})\n"; return $this->teams[$id] ?? null; } public function findTeamsByIds(array $ids): array { $this->queryCount++; echo " šŸ“Š Query #{$this->queryCount}: findTeamsByIds([" . implode(', ', $ids) . "])\n"; $result = []; foreach ($ids as $id) { if (isset($this->teams[$id])) { $result[$id] = $this->teams[$id]; } } return $result; } public function getAllUsers(): array { $this->queryCount++; echo " šŸ“Š Query #{$this->queryCount}: getAllUsers()\n"; return array_values($this->users); } public function resetQueryCount(): void { $this->queryCount = 0; } } $db = new MockDatabase(); // GraphQL Types #[GraphQLType(description: 'A team')] final readonly class Team { public function __construct( public int $id, public string $name ) { } } #[GraphQLType(description: 'A user')] final class User { private static ?MockDatabase $database = null; public function __construct( public int $id, public string $name, public int $teamId ) { } public static function setDatabase(MockDatabase $database): void { self::$database = $database; } #[GraphQLField(description: 'User team')] public function team(ExecutionContext $context): ?Team { // Use DataLoader to batch team loading $teamLoader = $context->loader('teams', function (array $teamIds) { return self::$database->findTeamsByIds($teamIds); }); $teamData = $teamLoader->load($this->teamId); if ($teamData === null) { return null; } return new Team($teamData['id'], $teamData['name']); } } // Query Resolvers #[GraphQLQuery] final class UserQueries { private static ?MockDatabase $database = null; public static function setDatabase(MockDatabase $database): void { self::$database = $database; } #[GraphQLField(description: 'Get all users')] public function users(ExecutionContext $context): array { $usersData = self::$database->getAllUsers(); return array_map( fn ($userData) => new User($userData['id'], $userData['name'], $userData['teamId']), $usersData ); } } // Set database for static access User::setDatabase($db); UserQueries::setDatabase($db); $schema = $schemaBuilder->build([User::class, Team::class, UserQueries::class]); $parser = new QueryParser(); $executor = new QueryExecutor($schema); // Test 1: Without DataLoader (N+1 Problem) echo "1. Testing WITHOUT DataLoader (N+1 Problem)...\n"; $query1 = <<<'GRAPHQL' { users { id name } } GRAPHQL; $db->resetQueryCount(); $parsed1 = $parser->parse($query1); $context1 = ExecutionContext::create(); $result1 = $executor->execute($parsed1, [], $context1); if (! $result1->isSuccessful()) { echo " āŒ Errors: " . json_encode($result1->errors, JSON_PRETTY_PRINT) . "\n"; } else { echo " āœ“ Query Count: {$db->queryCount} (Expected: 1)\n"; echo " āœ“ Result: " . json_encode($result1->data, JSON_PRETTY_PRINT) . "\n"; } echo "\n"; // Test 2: With DataLoader (Batched Queries) echo "2. Testing WITH DataLoader (Batched Queries)...\n"; $query2 = <<<'GRAPHQL' { users { id name team { id name } } } GRAPHQL; $db->resetQueryCount(); $parsed2 = $parser->parse($query2); $context2 = ExecutionContext::create(); $result2 = $executor->execute($parsed2, [], $context2); if (! $result2->isSuccessful()) { echo " āŒ Errors: " . json_encode($result2->errors, JSON_PRETTY_PRINT) . "\n"; } else { echo " āœ“ Query Count: {$db->queryCount} (Expected: 2 instead of 5)\n"; echo " āœ“ Without DataLoader would be: 1 (getAllUsers) + 4 (findTeam per user) = 5 queries\n"; echo " āœ“ With DataLoader: 1 (getAllUsers) + 1 (findTeamsByIds batched) = 2 queries\n"; echo " āœ“ Result: " . json_encode($result2->data, JSON_PRETTY_PRINT) . "\n"; } echo "\n"; // Test 3: Verify DataLoader Caching echo "3. Testing DataLoader Caching...\n"; $query3 = <<<'GRAPHQL' { users { id name team { id name } } } GRAPHQL; $db->resetQueryCount(); $parsed3 = $parser->parse($query3); $context3 = ExecutionContext::create(); // Prime the cache for team 10 $teamLoader = $context3->loader('teams', function (array $teamIds) use ($db) { return $db->findTeamsByIds($teamIds); }); $teamLoader->prime(10, ['id' => 10, 'name' => 'Engineering']); $result3 = $executor->execute($parsed3, [], $context3); echo " āœ“ Query Count: {$db->queryCount} (Expected: 2 - team 10 was cached)\n"; echo " āœ“ Only team 20 needs to be loaded from database\n\n"; // Test 4: Multiple DataLoaders echo "4. Testing Multiple DataLoaders...\n"; $query4 = <<<'GRAPHQL' { users { id name team { id name } } } GRAPHQL; $db->resetQueryCount(); $parsed4 = $parser->parse($query4); $context4 = ExecutionContext::create(); // Register multiple loaders $teamLoader1 = $context4->loader('teams', function (array $ids) use ($db) { return $db->findTeamsByIds($ids); }); $userLoader = $context4->loader('users', function (array $ids) use ($db) { return $db->findUsersByIds($ids); }); $result4 = $executor->execute($parsed4, [], $context4); echo " āœ“ Query Count: {$db->queryCount}\n"; echo " āœ“ Both loaders registered and dispatched\n\n"; // Test 5: DataLoader Statistics echo "5. Testing DataLoader Statistics...\n"; $context5 = ExecutionContext::create(); $testLoader = $context5->loader('test', function (array $ids) { return array_combine($ids, array_map(fn ($id) => "value-{$id}", $ids)); }); // Queue some loads $testLoader->load(1); $testLoader->load(2); $testLoader->load(3); $testLoader->load(2); // Duplicate $stats = $testLoader->getStats(); echo " āœ“ Cached Count: {$stats['cached_count']}\n"; echo " āœ“ Queued Count: {$stats['queued_count']}\n"; echo " āœ“ Dispatched: " . ($stats['dispatched'] ? 'Yes' : 'No') . "\n"; $testLoader->dispatch(); $statsAfter = $testLoader->getStats(); echo " āœ“ After Dispatch - Cached Count: {$statsAfter['cached_count']}\n"; echo " āœ“ After Dispatch - Queued Count: {$statsAfter['queued_count']}\n"; echo " āœ“ After Dispatch - Dispatched: " . ($statsAfter['dispatched'] ? 'Yes' : 'No') . "\n\n"; // Test 6: Auto-dispatch on threshold echo "6. Testing Auto-dispatch on Queue Threshold...\n"; $context6 = ExecutionContext::create(); $autoLoader = $context6->loader('auto', function (array $ids) { echo " ⚔ Auto-dispatched batch of " . count($ids) . " items\n"; return array_combine($ids, array_map(fn ($id) => "auto-{$id}", $ids)); }); // Queue 100 items (should auto-dispatch) for ($i = 1; $i <= 100; $i++) { $autoLoader->load($i); } $statsAuto = $autoLoader->getStats(); echo " āœ“ Auto-dispatch triggered at 100 items\n"; echo " āœ“ Dispatched: " . ($statsAuto['dispatched'] ? 'Yes' : 'No') . "\n\n"; echo "āœ… All DataLoader Tests Passed!\n"; echo "\nšŸ“Š Summary:\n"; echo " • DataLoader successfully batches multiple load() calls\n"; echo " • N+1 queries reduced from 5 to 2 queries (60% reduction)\n"; echo " • Caching prevents duplicate database queries\n"; echo " • Multiple loaders work independently\n"; echo " • Statistics tracking works correctly\n"; echo " • Auto-dispatch triggers at queue threshold\n";