diff --git a/src/Compiler/ServiceInvocationGenerator.php b/src/Compiler/ServiceInvocationGenerator.php index ac05e83..3f82ec1 100644 --- a/src/Compiler/ServiceInvocationGenerator.php +++ b/src/Compiler/ServiceInvocationGenerator.php @@ -5,8 +5,12 @@ namespace Maduser\Argon\Container\Compiler; use Maduser\Argon\Container\ArgonContainer; +use Maduser\Argon\Container\ServiceDescriptor; use Maduser\Argon\Container\Support\StringHelper; use Nette\PhpGenerator\ClassType; +use ReflectionException; +use ReflectionMethod; +use ReflectionNamedType; final class ServiceInvocationGenerator { @@ -33,12 +37,21 @@ public function generate(ClassType $class): void } } - $mergedArgsLine = 'array_merge([' . implode(", ", $compiledArgs) . '], $args)'; - $body = <<{$method}(...\$mergedArgs); - PHP; + $casts = $this->buildPrimitiveCastLines($descriptor, $method); + $indent = str_repeat(' ', 20); + + $lines = [ + $indent . $controllerFetch, + $indent . '$mergedArgs = ' . 'array_merge([' . implode(", ", $compiledArgs) . '], $args);', + ]; + + foreach ($casts as $castLine) { + $lines[] = $indent . $castLine; + } + + $lines[] = $indent . "return \$controller->{$method}(...\$mergedArgs);"; + + $body = implode("\n", $lines); $class->addMethod($compiledMethodName) ->setPublic() @@ -56,4 +69,49 @@ private function buildMethodInvokerName(string $serviceId, string $method): stri return 'invoke_' . $sanitizedService . '__' . $sanitizedMethod; } + + /** + * @return list + */ + private function buildPrimitiveCastLines(ServiceDescriptor $descriptor, string $method): array + { + $concrete = $descriptor->getConcrete(); + + if (!is_string($concrete) || !class_exists($concrete)) { + return []; + } + + try { + $reflection = new ReflectionMethod($concrete, $method); + } catch (ReflectionException) { + return []; + } + + $casts = []; + + foreach ($reflection->getParameters() as $parameter) { + $type = $parameter->getType(); + + if (!$type instanceof ReflectionNamedType || !$type->isBuiltin()) { + continue; + } + + $name = $parameter->getName(); + $condition = "array_key_exists('{$name}', \$mergedArgs) && \$mergedArgs['{$name}'] !== null"; + + switch ($type->getName()) { + case 'int': + $casts[] = "if ({$condition}) { \$mergedArgs['{$name}'] = (int) \$mergedArgs['{$name}']; }"; + break; + case 'float': + case 'double': + $casts[] = "if ({$condition}) { \$mergedArgs['{$name}'] = (float) \$mergedArgs['{$name}']; }"; + break; + default: + break; + } + } + + return $casts; + } } diff --git a/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php b/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php new file mode 100644 index 0000000..ae159d5 --- /dev/null +++ b/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php @@ -0,0 +1,92 @@ +set(ServiceWithTypedMethods::class) + ->defineInvocation('handle', [ + 'id' => '42', + 'ratio' => '3.5', + 'dependency' => '@dependency.service', + 'note' => 'test', + ]); + $container->set('dependency.service', static fn() => new ServiceDependency()); + + $generator = new ServiceInvocationGenerator($container); + $class = new ClassType('CompiledContainer'); + + $generator->generate($class); + + $methodName = 'invoke_' + . StringHelper::sanitizeIdentifier(ServiceWithTypedMethods::class) + . '__handle'; + + self::assertTrue($class->hasMethod($methodName)); + $method = $class->getMethod($methodName); + self::assertSame('mixed', $method->getReturnType()); + + $parameters = $method->getParameters(); + self::assertArrayHasKey('args', $parameters); + $argsParameter = $parameters['args']; + self::assertSame('array', $argsParameter->getType()); + self::assertTrue($argsParameter->hasDefaultValue()); + self::assertSame([], $argsParameter->getDefaultValue()); + + $body = $method->getBody(); + $escapedServiceClass = str_replace('\\', '\\\\', ServiceWithTypedMethods::class); + self::assertStringContainsString("\$controller = \$this->get('{$escapedServiceClass}');", $body); + self::assertStringContainsString("\$this->get('dependency.service')", $body); + self::assertStringContainsString("(int) \$mergedArgs['id']", $body); + self::assertStringContainsString("(float) \$mergedArgs['ratio']", $body); + } + + public function testGenerateSkipsPrimitiveCastsForNonClassAndMissingMethod(): void + { + $container = new ArgonContainer(); + $container->set('closure.service', static fn() => new ServiceWithTypedMethods()) + ->defineInvocation('handle', ['id' => 1]); + $container->set(ServiceWithTypedMethods::class) + ->defineInvocation('undefinedMethod', []); + + $generator = new ServiceInvocationGenerator($container); + $class = new ClassType('CompiledContainer'); + + $generator->generate($class); + + $closureInvokerName = 'invoke_' + . StringHelper::sanitizeIdentifier('closure.service') + . '__handle'; + $missingInvokerName = 'invoke_' + . StringHelper::sanitizeIdentifier(ServiceWithTypedMethods::class) + . '__undefinedMethod'; + + self::assertTrue($class->hasMethod($closureInvokerName)); + self::assertTrue($class->hasMethod($missingInvokerName)); + $closureMethod = $class->getMethod($closureInvokerName); + $missingMethod = $class->getMethod($missingInvokerName); + + $escapedServiceClass = str_replace('\\', '\\\\', ServiceWithTypedMethods::class); + + $closureBody = $closureMethod->getBody(); + self::assertStringNotContainsString('(int)', $closureBody); + self::assertStringNotContainsString('(float)', $closureBody); + + $missingBody = $missingMethod->getBody(); + self::assertStringContainsString("\$controller = \$this->get('{$escapedServiceClass}');", $missingBody); + self::assertStringContainsString("\$controller->undefinedMethod(...\$mergedArgs);", $missingBody); + } +} diff --git a/tests/unit/Container/Compiler/Stubs/ServiceDependency.php b/tests/unit/Container/Compiler/Stubs/ServiceDependency.php new file mode 100644 index 0000000..205c76b --- /dev/null +++ b/tests/unit/Container/Compiler/Stubs/ServiceDependency.php @@ -0,0 +1,9 @@ +