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:
281
tests/Framework/DI/ContainerCompilerTest.php
Normal file
281
tests/Framework/DI/ContainerCompilerTest.php
Normal 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}/');
|
||||
});
|
||||
219
tests/Framework/DI/ContainerMemoryLeakTest.php
Normal file
219
tests/Framework/DI/ContainerMemoryLeakTest.php
Normal 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";
|
||||
}
|
||||
}
|
||||
239
tests/Framework/DI/DefaultContainerTest.php
Normal file
239
tests/Framework/DI/DefaultContainerTest.php
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user