From 4018e98b09d321decb3819d708e96b21fe9c114d Mon Sep 17 00:00:00 2001 From: Julien Duseyau Date: Tue, 28 Oct 2025 17:14:53 +0100 Subject: [PATCH 1/3] Enhance ServiceInvocationGenerator to include primitive type casting for method parameters --- src/Compiler/ServiceInvocationGenerator.php | 70 +++++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) 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; + } } From 24431fe5c3b2c4bd5a5db1dfefc60fbd0aa82388 Mon Sep 17 00:00:00 2001 From: Julien Duseyau Date: Tue, 28 Oct 2025 17:35:39 +0100 Subject: [PATCH 2/3] Add ServiceDependency and ServiceWithTypedMethods stubs with typed method handling tests --- .../ServiceInvocationGeneratorTest.php | 97 +++++++++++++++++++ .../Compiler/Stubs/ServiceDependency.php | 9 ++ .../Stubs/ServiceWithTypedMethods.php | 13 +++ 3 files changed, 119 insertions(+) create mode 100644 tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php create mode 100644 tests/unit/Container/Compiler/Stubs/ServiceDependency.php create mode 100644 tests/unit/Container/Compiler/Stubs/ServiceWithTypedMethods.php diff --git a/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php b/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php new file mode 100644 index 0000000..1bd1964 --- /dev/null +++ b/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php @@ -0,0 +1,97 @@ +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'; + + $method = $class->getMethod($methodName); + + self::assertNotNull($method); + 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(); + self::assertIsString($body); + $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'; + + $closureMethod = $class->getMethod($closureInvokerName); + $missingMethod = $class->getMethod($missingInvokerName); + + self::assertNotNull($closureMethod); + self::assertNotNull($missingMethod); + + $escapedServiceClass = str_replace('\\', '\\\\', ServiceWithTypedMethods::class); + + $closureBody = $closureMethod->getBody(); + self::assertIsString($closureBody); + self::assertStringNotContainsString('(int)', $closureBody); + self::assertStringNotContainsString('(float)', $closureBody); + + $missingBody = $missingMethod->getBody(); + self::assertIsString($missingBody); + 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 @@ + Date: Tue, 28 Oct 2025 18:13:03 +0100 Subject: [PATCH 3/3] Psalm --- .../Compiler/ServiceInvocationGeneratorTest.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php b/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php index 1bd1964..ae159d5 100644 --- a/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php +++ b/tests/unit/Container/Compiler/ServiceInvocationGeneratorTest.php @@ -35,9 +35,8 @@ public function testGenerateCreatesInvokerWithCastsAndServiceReferences(): void . StringHelper::sanitizeIdentifier(ServiceWithTypedMethods::class) . '__handle'; + self::assertTrue($class->hasMethod($methodName)); $method = $class->getMethod($methodName); - - self::assertNotNull($method); self::assertSame('mixed', $method->getReturnType()); $parameters = $method->getParameters(); @@ -48,7 +47,6 @@ public function testGenerateCreatesInvokerWithCastsAndServiceReferences(): void self::assertSame([], $argsParameter->getDefaultValue()); $body = $method->getBody(); - self::assertIsString($body); $escapedServiceClass = str_replace('\\', '\\\\', ServiceWithTypedMethods::class); self::assertStringContainsString("\$controller = \$this->get('{$escapedServiceClass}');", $body); self::assertStringContainsString("\$this->get('dependency.service')", $body); @@ -76,21 +74,18 @@ public function testGenerateSkipsPrimitiveCastsForNonClassAndMissingMethod(): vo . StringHelper::sanitizeIdentifier(ServiceWithTypedMethods::class) . '__undefinedMethod'; + self::assertTrue($class->hasMethod($closureInvokerName)); + self::assertTrue($class->hasMethod($missingInvokerName)); $closureMethod = $class->getMethod($closureInvokerName); $missingMethod = $class->getMethod($missingInvokerName); - self::assertNotNull($closureMethod); - self::assertNotNull($missingMethod); - $escapedServiceClass = str_replace('\\', '\\\\', ServiceWithTypedMethods::class); $closureBody = $closureMethod->getBody(); - self::assertIsString($closureBody); self::assertStringNotContainsString('(int)', $closureBody); self::assertStringNotContainsString('(float)', $closureBody); $missingBody = $missingMethod->getBody(); - self::assertIsString($missingBody); self::assertStringContainsString("\$controller = \$this->get('{$escapedServiceClass}');", $missingBody); self::assertStringContainsString("\$controller->undefinedMethod(...\$mergedArgs);", $missingBody); }