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) { } }