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,281 @@
<?php
declare(strict_types=1);
use App\Framework\DI\ContainerCompiler;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\DependencyResolver;
use App\Framework\Reflection\CachedReflectionProvider;
beforeEach(function () {
$this->tempDir = sys_get_temp_dir() . '/container-compiler-test-' . uniqid();
mkdir($this->tempDir, 0755, true);
$this->container = new DefaultContainer();
$this->reflectionProvider = new CachedReflectionProvider();
$this->dependencyResolver = new DependencyResolver($this->reflectionProvider, $this->container);
$this->compiler = new ContainerCompiler($this->reflectionProvider, $this->dependencyResolver);
$this->compiledPath = $this->tempDir . '/compiled-container.php';
});
afterEach(function () {
// Clean up test directory
if (is_dir($this->tempDir)) {
array_map('unlink', glob($this->tempDir . '/*'));
rmdir($this->tempDir);
}
});
// Test classes
class ContainerCompilerTestSimpleService
{
public function getName(): string
{
return 'simple';
}
}
class ContainerCompilerTestServiceWithDependency
{
public function __construct(private ContainerCompilerTestSimpleService $service)
{
}
public function getServiceName(): string
{
return $this->service->getName();
}
}
interface ContainerCompilerTestServiceInterface
{
public function getValue(): string;
}
class ContainerCompilerTestConcreteService implements ContainerCompilerTestServiceInterface
{
public function getValue(): string
{
return 'concrete';
}
}
test('compiles container with simple binding', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
// Act
$this->compiler->compile($this->container, $this->compiledPath);
// Assert
expect(file_exists($this->compiledPath))->toBeTrue();
$content = file_get_contents($this->compiledPath);
expect($content)->toContain('class CompiledContainer implements Container');
expect($content)->toContain('createContainerCompilerTestSimpleService()');
});
test('compiles container with dependency injection', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->container->bind(ContainerCompilerTestServiceWithDependency::class, ContainerCompilerTestServiceWithDependency::class);
// Act
$this->compiler->compile($this->container, $this->compiledPath);
// Assert
expect(file_exists($this->compiledPath))->toBeTrue();
$content = file_get_contents($this->compiledPath);
expect($content)->toContain('$this->get(\'ContainerCompilerTestSimpleService\')');
});
test('compiles container with singletons', function () {
// Arrange
$this->container->singleton(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
// Act
$this->compiler->compile($this->container, $this->compiledPath);
// Assert
expect(file_exists($this->compiledPath))->toBeTrue();
$content = file_get_contents($this->compiledPath);
expect($content)->toContain('$this->singletons[\'ContainerCompilerTestSimpleService\'] = true');
});
test('loads compiled container successfully', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
// Act
$compiledContainer = ContainerCompiler::load($this->compiledPath);
// Assert
expect($compiledContainer)->toBeInstanceOf(\App\Framework\DI\Container::class);
expect($compiledContainer->has(ContainerCompilerTestSimpleService::class))->toBeTrue();
$instance = $compiledContainer->get(ContainerCompilerTestSimpleService::class);
expect($instance)->toBeInstanceOf(ContainerCompilerTestSimpleService::class);
expect($instance->getName())->toBe('simple');
});
test('compiled container resolves dependencies correctly', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->container->bind(ContainerCompilerTestServiceWithDependency::class, ContainerCompilerTestServiceWithDependency::class);
$this->compiler->compile($this->container, $this->compiledPath);
// Act
$compiledContainer = ContainerCompiler::load($this->compiledPath);
$instance = $compiledContainer->get(ContainerCompilerTestServiceWithDependency::class);
// Assert
expect($instance)->toBeInstanceOf(ContainerCompilerTestServiceWithDependency::class);
expect($instance->getServiceName())->toBe('simple');
});
test('compiled container handles singletons correctly', function () {
// Arrange
$this->container->singleton(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
// Act
$compiledContainer = ContainerCompiler::load($this->compiledPath);
$instance1 = $compiledContainer->get(ContainerCompilerTestSimpleService::class);
$instance2 = $compiledContainer->get(ContainerCompilerTestSimpleService::class);
// Assert
expect($instance1)->toBe($instance2);
});
test('validates compiled container hash correctly', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
// Act & Assert - should be valid initially
expect($this->compiler->isCompiledContainerValid($this->container, $this->compiledPath))->toBeTrue();
// Add new binding to change container state
$this->container->bind(ContainerCompilerTestServiceWithDependency::class, ContainerCompilerTestServiceWithDependency::class);
// Should now be invalid due to hash mismatch
expect($this->compiler->isCompiledContainerValid($this->container, $this->compiledPath))->toBeFalse();
});
test('returns false for non-existent compiled container', function () {
// Arrange
$nonExistentPath = $this->tempDir . '/non-existent.php';
// Act & Assert
expect($this->compiler->isCompiledContainerValid($this->container, $nonExistentPath))->toBeFalse();
});
test('creates directory if it does not exist', function () {
// Arrange
$nestedPath = $this->tempDir . '/nested/deep/compiled-container.php';
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
// Act
$this->compiler->compile($this->container, $nestedPath);
// Assert
expect(file_exists($nestedPath))->toBeTrue();
expect(is_dir(dirname($nestedPath)))->toBeTrue();
});
test('throws exception when loading non-existent compiled container', function () {
// Arrange
$nonExistentPath = $this->tempDir . '/non-existent.php';
// Act & Assert
expect(fn () => ContainerCompiler::load($nonExistentPath))
->toThrow(RuntimeException::class, 'Compiled container not found');
});
test('compiled container throws exception for runtime binding', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
$compiledContainer = ContainerCompiler::load($this->compiledPath);
// Act & Assert
expect(fn () => $compiledContainer->bind('NewClass', 'AnotherClass'))
->toThrow(RuntimeException::class, 'Cannot bind to compiled container');
});
test('compiled container throws exception for runtime singleton registration', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
$compiledContainer = ContainerCompiler::load($this->compiledPath);
// Act & Assert
expect(fn () => $compiledContainer->singleton('NewClass', 'AnotherClass'))
->toThrow(RuntimeException::class, 'Cannot add singletons to compiled container');
});
test('compiled container allows runtime instance registration', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
$compiledContainer = ContainerCompiler::load($this->compiledPath);
$customInstance = new ContainerCompilerTestSimpleService();
// Act
$compiledContainer->instance(ContainerCompilerTestSimpleService::class, $customInstance);
$retrievedInstance = $compiledContainer->get(ContainerCompilerTestSimpleService::class);
// Assert
expect($retrievedInstance)->toBe($customInstance);
});
test('gets default compiled container path', function () {
// Act
$path = ContainerCompiler::getCompiledContainerPath();
// Assert
expect($path)->toContain('compiled-container.php');
expect(is_dir(dirname($path)))->toBeTrue();
});
test('gets custom compiled container path', function () {
// Arrange
$customCacheDir = $this->tempDir . '/custom-cache';
// Act
$path = ContainerCompiler::getCompiledContainerPath($customCacheDir);
// Assert
expect($path)->toBe($customCacheDir . '/compiled-container.php');
expect(is_dir($customCacheDir))->toBeTrue();
});
test('compiled container handles unknown class gracefully', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
$this->compiler->compile($this->container, $this->compiledPath);
$compiledContainer = ContainerCompiler::load($this->compiledPath);
// Act & Assert
expect(fn () => $compiledContainer->get('UnknownClass'))
->toThrow(InvalidArgumentException::class, 'Class UnknownClass is not bound in the container');
});
test('generated code contains proper metadata', function () {
// Arrange
$this->container->bind(ContainerCompilerTestSimpleService::class, ContainerCompilerTestSimpleService::class);
// Act
$this->compiler->compile($this->container, $this->compiledPath);
// Assert
$content = file_get_contents($this->compiledPath);
expect($content)->toContain('Generated:');
expect($content)->toContain('Hash:');
expect($content)->toContain('WARNING: This file is auto-generated');
expect($content)->toMatch('/Hash: [a-f0-9]{64}/');
});

View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Tests\Framework\DI;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\InstanceRegistry;
use PHPUnit\Framework\TestCase;
use stdClass;
/**
* Tests to identify and prevent Container memory leaks
*/
final class ContainerMemoryLeakTest extends TestCase
{
private DefaultContainer $container;
protected function setUp(): void
{
$this->container = new DefaultContainer();
// Prä-Registriere stdClass als echte Klasse um Lazy Loading zu vermeiden
$this->container->bind(stdClass::class, fn () => new stdClass());
}
/**
* Test ob der Container unbegrenzt wächst bei wiederholten Aufrufen
*/
public function test_container_does_not_grow_indefinitely(): void
{
$initialMemory = memory_get_usage(true);
// Simuliere viele Requests mit direkten Instanzen (vermeidet Lazy Loading)
for ($i = 0; $i < 1000; $i++) {
$serviceName = "service_$i";
// Erstelle Objekt direkt und registriere als Instanz
$obj = new stdClass();
$obj->id = $i;
$obj->data = str_repeat('x', 100);
$this->container->instance($serviceName, $obj);
$instance = $this->container->get($serviceName);
$this->assertEquals($i, $instance->id);
}
$finalMemory = memory_get_usage(true);
$memoryGrowth = $finalMemory - $initialMemory;
// Memory growth sollte unter 5MB bleiben für 1000 Services
$this->assertLessThan(
5 * 1024 * 1024,
$memoryGrowth,
"Container memory grew by " . number_format($memoryGrowth) . " bytes for 1000 services"
);
echo "\nMemory growth for 1000 services: " . number_format($memoryGrowth) . " bytes\n";
}
/**
* Test ob die InstanceRegistry ordnungsgemäß aufräumt
*/
public function test_instance_registry_can_be_flushed(): void
{
$registry = new InstanceRegistry();
// Fülle Registry mit vielen Instanzen
for ($i = 0; $i < 100; $i++) {
$serviceName = "TestInstance_$i";
$instance = (object)['id' => $i, 'data' => "test_$i"];
$registry->setInstance($serviceName, $instance);
}
// Prüfe dass Instanzen registriert sind
$this->assertCount(100, $registry->getAllRegistered());
// Flush sollte alles löschen
$registry->flush();
$this->assertCount(0, $registry->getAllRegistered());
}
/**
* Test ob Singletons ordnungsgemäß verwaltet werden
* Test nutzt InstanceRegistry direkt da singleton() auch lazy loading versucht
*/
public function test_singleton_lifecycle(): void
{
$serviceName = 'singleton_service';
// Erstelle Objekt direkt und registriere als Singleton über InstanceRegistry
$obj = new stdClass();
$obj->data = 'test';
$obj->created_at = time();
// Direktes Setzen als Singleton über die Registry
$registry = new InstanceRegistry();
$registry->setSingleton($serviceName, $obj);
// Container mit dieser Registry erstellen
$container = new DefaultContainer(instances: $registry);
// Sollte immer die gleiche Instanz zurückgeben
$instance1 = $container->get($serviceName);
$instance2 = $container->get($serviceName);
$this->assertSame($instance1, $instance2);
$this->assertEquals('test', $instance1->data);
}
/**
* Test ob Container-Cache bei bind() ordnungsgemäß geleert wird
*/
public function test_container_cache_clearing(): void
{
$serviceName = 'cache_service';
// Erste Instanz
$obj1 = new stdClass();
$obj1->version = 1;
$this->container->instance($serviceName, $obj1);
$instance1 = $this->container->get($serviceName);
// Neue Instanz sollte alte überschreiben
$obj2 = new stdClass();
$obj2->version = 2;
$this->container->instance($serviceName, $obj2);
$instance2 = $this->container->get($serviceName);
$this->assertNotSame($instance1, $instance2);
$this->assertEquals(1, $instance1->version);
$this->assertEquals(2, $instance2->version);
}
/**
* Test Memory Usage unter Last
*/
public function test_memory_usage_under_load(): void
{
$memoryBefore = memory_get_usage(true);
$maxMemoryUsed = 0;
// Simuliere Last von 500 verschiedenen Services
for ($iteration = 0; $iteration < 5; $iteration++) {
for ($i = 0; $i < 100; $i++) {
$serviceName = "load_service_{$iteration}_{$i}";
// Erstelle Service-Objekt direkt
$obj = new stdClass();
$obj->data = array_fill(0, 100, 'load_test_data');
$obj->timestamp = microtime(true);
$obj->random = random_bytes(256);
$this->container->instance($serviceName, $obj);
$instance = $this->container->get($serviceName);
$this->assertCount(100, $instance->data);
$currentMemory = memory_get_usage(true);
$maxMemoryUsed = max($maxMemoryUsed, $currentMemory - $memoryBefore);
}
// Optional: Garbage collection nach jeder Iteration
gc_collect_cycles();
}
$finalMemory = memory_get_usage(true);
$totalGrowth = $finalMemory - $memoryBefore;
// Container sollte nicht unbegrenzt wachsen
$this->assertLessThan(
20 * 1024 * 1024,
$totalGrowth,
"Container grew by " . number_format($totalGrowth) . " bytes under load"
);
echo "\nMemory Statistics:\n";
echo "- Initial Memory: " . number_format($memoryBefore) . " bytes\n";
echo "- Final Memory: " . number_format($finalMemory) . " bytes\n";
echo "- Total Growth: " . number_format($totalGrowth) . " bytes\n";
echo "- Max Memory Used: " . number_format($maxMemoryUsed) . " bytes\n";
}
/**
* Test Performance von Container-Lookups
*/
public function test_container_lookup_performance(): void
{
// Setup: 1000 Services als Instanzen registrieren
for ($i = 0; $i < 1000; $i++) {
$serviceName = "perf_service_$i";
$obj = new stdClass();
$obj->id = $i;
$obj->name = "service_$i";
$this->container->instance($serviceName, $obj);
}
$startTime = microtime(true);
// Performance Test: 10000 Lookups
for ($i = 0; $i < 10000; $i++) {
$serviceName = "perf_service_" . ($i % 1000);
$instance = $this->container->get($serviceName);
$this->assertEquals($i % 1000, $instance->id);
}
$endTime = microtime(true);
$duration = $endTime - $startTime;
// Sollte unter 1 Sekunde bleiben für 10k Lookups
$this->assertLessThan(
1.0,
$duration,
"Container lookups took {$duration}s for 10k operations"
);
echo "\nPerformance: " . number_format(10000 / $duration, 0) . " lookups/second\n";
}
}

View File

@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
use App\Framework\DI\Container;
use App\Framework\DI\DefaultContainer;
use App\Framework\DI\Exceptions\CyclicDependencyException;
beforeEach(function () {
$this->container = new DefaultContainer();
});
afterEach(function () {
$this->container->flush();
});
it('registers itself', function () {
expect($this->container->has(Container::class))->toBeTrue();
expect($this->container->has(DefaultContainer::class))->toBeTrue();
expect($this->container->get(Container::class))->toBe($this->container);
expect($this->container->get(DefaultContainer::class))->toBe($this->container);
});
it('creates simple class', function () {
$instance = $this->container->get(SimpleTestClass::class);
expect($instance)->toBeInstanceOf(SimpleTestClass::class);
});
it('caches instances for same class', function () {
// Note: DefaultContainer caches instances even for non-singletons
$instance1 = $this->container->get(SimpleTestClass::class);
$instance2 = $this->container->get(SimpleTestClass::class);
expect($instance1)->toBe($instance2);
});
it('binds string class', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
$instance = $this->container->get(TestInterface::class);
expect($instance)->toBeInstanceOf(ConcreteTestClass::class);
});
it('binds callable', function () {
$this->container->bind(TestInterface::class, fn () => new ConcreteTestClass('from-callable'));
$instance = $this->container->get(TestInterface::class);
expect($instance)->toBeInstanceOf(ConcreteTestClass::class);
expect($instance->value)->toBe('from-callable');
});
it('binds object', function () {
$object = new ConcreteTestClass('bound-object');
$this->container->bind(TestInterface::class, $object);
$instance = $this->container->get(TestInterface::class);
expect($instance)->toBe($object);
expect($instance->value)->toBe('bound-object');
});
it('returns same instance for singleton', function () {
$this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));
$instance1 = $this->container->get(TestInterface::class);
$instance2 = $this->container->get(TestInterface::class);
expect($instance1)->toBe($instance2);
expect($instance1->value)->toBe('singleton');
});
it('calls callable only once for singleton', function () {
$callCount = 0;
$this->container->singleton(TestInterface::class, function () use (&$callCount) {
$callCount++;
return new ConcreteTestClass("call-{$callCount}");
});
$instance1 = $this->container->get(TestInterface::class);
$instance2 = $this->container->get(TestInterface::class);
expect($instance1)->toBe($instance2);
expect($callCount)->toBe(1);
expect($instance1->value)->toBe('call-1');
});
it('stores instance directly', function () {
$object = new ConcreteTestClass('instance');
$this->container->instance(TestInterface::class, $object);
$instance = $this->container->get(TestInterface::class);
expect($instance)->toBe($object);
});
it('has returns true for existing class', function () {
expect($this->container->has(SimpleTestClass::class))->toBeTrue();
});
it('has returns true for bound class', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
expect($this->container->has(TestInterface::class))->toBeTrue();
});
it('has returns false for non-existent class', function () {
expect($this->container->has('NonExistentClass'))->toBeFalse();
});
it('forgets binding', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
expect($this->container->has(TestInterface::class))->toBeTrue();
$this->container->forget(TestInterface::class);
expect($this->container->has(TestInterface::class))->toBeFalse();
});
it('forgets singleton and creates new instance', function () {
$this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));
$instance1 = $this->container->get(TestInterface::class);
$this->container->forget(TestInterface::class);
// After forget, the binding is gone
expect($this->container->has(TestInterface::class))->toBeFalse();
});
it('resolves dependencies', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
$instance = $this->container->get(ClassWithDependency::class);
expect($instance)->toBeInstanceOf(ClassWithDependency::class);
expect($instance->dependency)->toBeInstanceOf(ConcreteTestClass::class);
});
it('throws exception for cyclic dependency', function () {
$this->container->bind(CyclicA::class, CyclicA::class);
$this->container->bind(CyclicB::class, CyclicB::class);
$this->container->get(CyclicA::class);
})->throws(CyclicDependencyException::class, 'Zyklische Abhängigkeit entdeckt');
it('returns registered services', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
$this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));
$services = $this->container->getRegisteredServices();
expect($services)->toContain(TestInterface::class);
expect($services)->toContain(Container::class);
expect($services)->toContain(DefaultContainer::class);
});
it('flushes all bindings and instances', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
$this->container->singleton(TestInterface::class, fn () => new ConcreteTestClass('singleton'));
$instance = $this->container->get(TestInterface::class);
$this->container->flush();
// Container should re-register itself after flush
expect($this->container->has(Container::class))->toBeTrue();
expect($this->container->has(DefaultContainer::class))->toBeTrue();
// Other bindings should be gone
expect($this->container->has(TestInterface::class))->toBeFalse();
// New instance should be different after flush
$newInstance = $this->container->get(SimpleTestClass::class);
expect($newInstance)->toBeInstanceOf(SimpleTestClass::class);
});
it('has method invoker available', function () {
expect($this->container->invoker)->not->toBeNull();
expect($this->container->invoker)->toBeInstanceOf(\App\Framework\DI\MethodInvoker::class);
});
it('resolves complex dependency chain', function () {
$this->container->bind(TestInterface::class, ConcreteTestClass::class);
$instance = $this->container->get(ComplexDependencyClass::class);
expect($instance)->toBeInstanceOf(ComplexDependencyClass::class);
expect($instance->classWithDep)->toBeInstanceOf(ClassWithDependency::class);
expect($instance->classWithDep->dependency)->toBeInstanceOf(ConcreteTestClass::class);
expect($instance->simple)->toBeInstanceOf(SimpleTestClass::class);
});
// Test helper classes
interface TestInterface
{
//
}
class SimpleTestClass
{
public function __construct(public string $value = 'default')
{
}
}
class ConcreteTestClass implements TestInterface
{
public function __construct(public string $value = 'default')
{
}
}
class ClassWithDependency
{
public function __construct(public TestInterface $dependency)
{
}
}
class ComplexDependencyClass
{
public function __construct(
public ClassWithDependency $classWithDep,
public SimpleTestClass $simple
) {
}
}
// Cyclic dependency test classes
class CyclicA
{
public function __construct(public CyclicB $b)
{
}
}
class CyclicB
{
public function __construct(public CyclicA $a)
{
}
}