container = $this->createMock(Container::class); $this->reflectionProvider = new CachedReflectionProvider(); $this->resolver = new ParameterResolver($this->container, $this->reflectionProvider); }); describe('ParameterResolver', function () { it('resolves parameters with LogChannel attribute', function () { // Mock main logger - use real DefaultLogger for simplicity $mainLogger = new DefaultLogger(); $this->container->expects($this->once()) ->method('get') ->with(SupportsChannels::class) ->willReturn($mainLogger); $className = ClassName::create(ServiceWithLogChannelAttribute::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBeInstanceOf(Logger::class); expect($params[0])->toBeInstanceOf(HasChannel::class); expect($params[0]->channel)->toBe(LogChannel::CACHE); }); it('resolves different channel loggers via attribute', function () { $mainLogger = new DefaultLogger(); $this->container->expects($this->exactly(3)) ->method('get') ->with(SupportsChannels::class) ->willReturn($mainLogger); $className = ClassName::create(ServiceWithMultipleLogChannels::class); $params = $this->resolver->resolveMethodParameters($className, 'testMethod'); expect($params)->toHaveCount(3); // Verify each logger has correct channel expect($params[0])->toBeInstanceOf(Logger::class); expect($params[0])->toBeInstanceOf(HasChannel::class); expect($params[0]->channel)->toBe(LogChannel::SECURITY); expect($params[1])->toBeInstanceOf(Logger::class); expect($params[1])->toBeInstanceOf(HasChannel::class); expect($params[1]->channel)->toBe(LogChannel::DATABASE); expect($params[2])->toBeInstanceOf(Logger::class); expect($params[2])->toBeInstanceOf(HasChannel::class); expect($params[2]->channel)->toBe(LogChannel::CACHE); }); it('resolves override values before attributes', function () { // Container should NOT be called because override takes priority $this->container->expects($this->never()) ->method('get'); $overrideLogger = $this->createMock(Logger::class); $className = ClassName::create(ServiceWithOverrideTest::class); $params = $this->resolver->resolveMethodParameters( $className, '__construct', ['logger' => $overrideLogger] ); expect($params)->toHaveCount(1); expect($params[0])->toBe($overrideLogger); }); it('resolves type-based injection when no attribute present', function () { $logger = $this->createMock(Logger::class); $this->container->expects($this->once()) ->method('has') ->with(Logger::class) ->willReturn(true); $this->container->expects($this->once()) ->method('get') ->with(Logger::class) ->willReturn($logger); $className = ClassName::create(ServiceWithTypedDependency::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBe($logger); }); it('resolves default values when no injection possible', function () { $this->container->expects($this->never()) ->method('get'); $className = ClassName::create(ServiceWithDefaultValue::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBe('default-value'); }); it('resolves nullable parameters to null', function () { $this->container->expects($this->never()) ->method('get'); $className = ClassName::create(ServiceWithNullable::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBeNull(); }); it('resolves nullable interface from container when available', function () { $logger = $this->createMock(Logger::class); $this->container->expects($this->once()) ->method('has') ->with(Logger::class) ->willReturn(true); $this->container->expects($this->once()) ->method('get') ->with(Logger::class) ->willReturn($logger); $className = ClassName::create(ServiceWithNullableInterfaceResolved::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBe($logger); }); it('resolves nullable interface to null when container does not have it', function () { $this->container->expects($this->exactly(2)) ->method('has') ->with(Logger::class) ->willReturn(false); $this->container->expects($this->never()) ->method('get'); $className = ClassName::create(ServiceWithNullableInterface::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBeNull(); }); it('throws exception when parameter cannot be resolved', function () { $this->container->method('has')->willReturn(false); $className = ClassName::create(ServiceWithRequiredParam::class); expect(fn () => $this->resolver->resolveMethodParameters($className, '__construct')) ->toThrow(\InvalidArgumentException::class, 'Cannot resolve parameter'); }); it('follows correct resolution priority order', function () { $mainLogger = new DefaultLogger(); $overrideLogger = $this->createMock(Logger::class); $containerLogger = $this->createMock(Logger::class); $this->container->method('get') ->willReturnCallback(function ($type) use ($mainLogger, $containerLogger) { return match ($type) { SupportsChannels::class => $mainLogger, Logger::class => $containerLogger, default => throw new \RuntimeException("Unexpected type: {$type}") }; }); $this->container->method('has') ->with(Logger::class) ->willReturn(true); $className = ClassName::create(ServiceWithMixedParams::class); $params = $this->resolver->resolveMethodParameters( $className, 'complexMethod', ['overriddenParam' => $overrideLogger] ); expect($params)->toHaveCount(5); // Attribute injection expect($params[0])->toBeInstanceOf(Logger::class); expect($params[0])->toBeInstanceOf(HasChannel::class); expect($params[0]->channel)->toBe(LogChannel::FRAMEWORK); // Override expect($params[1])->toBe($overrideLogger); // Container expect($params[2])->toBe($containerLogger); // Default value expect($params[3])->toBe('default'); // Nullable expect($params[4])->toBeNull(); }); it('resolves Env attribute from environment', function () { $environment = $this->createMock(Environment::class); $environment->expects($this->once()) ->method('getString') ->with(EnvKey::APP_NAME, '') ->willReturn('Test App Name'); $this->container->expects($this->once()) ->method('get') ->with(Environment::class) ->willReturn($environment); $className = ClassName::create(ServiceWithEnvAttribute::class); $params = $this->resolver->resolveMethodParameters($className, '__construct'); expect($params)->toHaveCount(1); expect($params[0])->toBe('Test App Name'); }); });