registry = $this->createMock(TenantRegistry::class); $this->clock = $this->createMock(Clock::class); $this->ormConfiguration = $this->createOrmConfiguration(); } #[Test] public function itReturnsEntityManagerForTenant(): void { $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $config = new TenantConfig( tenantId: $tenantId, subdomain: 'ecole-alpha', databaseUrl: 'sqlite:///:memory:', ); $this->registry->method('getConfig')->with($tenantId)->willReturn($config); $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00')); $factory = new TenantEntityManagerFactory( $this->registry, $this->clock, $this->ormConfiguration, ); $entityManager = $factory->getForTenant($tenantId); self::assertInstanceOf(EntityManagerInterface::class, $entityManager); } #[Test] public function itReturnsSameEntityManagerForSameTenant(): void { $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $config = new TenantConfig( tenantId: $tenantId, subdomain: 'ecole-alpha', databaseUrl: 'sqlite:///:memory:', ); $this->registry->method('getConfig')->with($tenantId)->willReturn($config); $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00')); $factory = new TenantEntityManagerFactory( $this->registry, $this->clock, $this->ormConfiguration, ); $em1 = $factory->getForTenant($tenantId); $em2 = $factory->getForTenant($tenantId); self::assertSame($em1, $em2); } #[Test] public function itReturnsDifferentEntityManagersForDifferentTenants(): void { $tenantIdAlpha = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $tenantIdBeta = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901'); $configAlpha = new TenantConfig( tenantId: $tenantIdAlpha, subdomain: 'ecole-alpha', databaseUrl: 'sqlite:///:memory:', ); $configBeta = new TenantConfig( tenantId: $tenantIdBeta, subdomain: 'ecole-beta', databaseUrl: 'sqlite:///:memory:', ); $this->registry->method('getConfig')->willReturnMap([ [$tenantIdAlpha, $configAlpha], [$tenantIdBeta, $configBeta], ]); $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00')); $factory = new TenantEntityManagerFactory( $this->registry, $this->clock, $this->ormConfiguration, ); $emAlpha = $factory->getForTenant($tenantIdAlpha); $emBeta = $factory->getForTenant($tenantIdBeta); self::assertNotSame($emAlpha, $emBeta); } #[Test] public function itReturnsCorrectPoolSize(): void { $tenantId1 = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $tenantId2 = TenantId::fromString('b2c3d4e5-f6a7-8901-bcde-f12345678901'); $config1 = new TenantConfig( tenantId: $tenantId1, subdomain: 'ecole-alpha', databaseUrl: 'sqlite:///:memory:', ); $config2 = new TenantConfig( tenantId: $tenantId2, subdomain: 'ecole-beta', databaseUrl: 'sqlite:///:memory:', ); $this->registry->method('getConfig')->willReturnMap([ [$tenantId1, $config1], [$tenantId2, $config2], ]); $this->clock->method('now')->willReturn(new DateTimeImmutable('2026-01-30 10:00:00')); $factory = new TenantEntityManagerFactory( $this->registry, $this->clock, $this->ormConfiguration, ); self::assertSame(0, $factory->getPoolSize()); $factory->getForTenant($tenantId1); self::assertSame(1, $factory->getPoolSize()); $factory->getForTenant($tenantId2); self::assertSame(2, $factory->getPoolSize()); // Accessing same tenant shouldn't increase pool size $factory->getForTenant($tenantId1); self::assertSame(2, $factory->getPoolSize()); } #[Test] public function itEvictsIdleConnectionsAfterTimeout(): void { $tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'); $config = new TenantConfig( tenantId: $tenantId, subdomain: 'ecole-alpha', databaseUrl: 'sqlite:///:memory:', ); $this->registry->method('getConfig')->with($tenantId)->willReturn($config); $initialTime = new DateTimeImmutable('2026-01-30 10:00:00'); $afterTimeout = new DateTimeImmutable('2026-01-30 10:06:00'); // 6 minutes later $this->clock->method('now')->willReturnOnConsecutiveCalls( $initialTime, // First call - eviction check $initialTime, // Store lastUsed $afterTimeout, // Second call - eviction check (finds idle) $afterTimeout, // Store lastUsed for new manager ); $factory = new TenantEntityManagerFactory( $this->registry, $this->clock, $this->ormConfiguration, ); $em1 = $factory->getForTenant($tenantId); $em2 = $factory->getForTenant($tenantId); // Due to idle eviction, we should have a new entity manager self::assertNotSame($em1, $em2); } private function createOrmConfiguration(): Configuration { $config = new Configuration(); $config->setProxyDir(sys_get_temp_dir() . '/doctrine_proxies_' . uniqid()); $config->setProxyNamespace('DoctrineProxies'); $config->setAutoGenerateProxyClasses(true); $config->setMetadataDriverImpl(new AttributeDriver([])); $config->enableNativeLazyObjects(true); return $config; } }