diff --git a/composer.json b/composer.json index 9a745549..6ec8f29c 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "satooshi/php-coveralls": "^1.0", "slevomat/coding-standard": "^4.5.2", "phpstan/phpstan-phpunit": "^0.10", - "symfony/framework-bundle": "^3.0 || ^4.0" + "symfony/framework-bundle": "^3.0 || ^4.0", + "symfony/form": "^3.0 || ^4.0" }, "conflict": { "symfony/framework-bundle": "<3.0" diff --git a/src/Type/Symfony/ControllerDynamicReturnTypeExtension.php b/src/Type/Symfony/ControllerDynamicReturnTypeExtension.php index 36536218..144b7920 100644 --- a/src/Type/Symfony/ControllerDynamicReturnTypeExtension.php +++ b/src/Type/Symfony/ControllerDynamicReturnTypeExtension.php @@ -5,9 +5,12 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Symfony\ServiceMap; use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; final class ControllerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -27,7 +30,7 @@ public function getClass(): string public function isMethodSupported(MethodReflection $methodReflection): bool { - return in_array($methodReflection->getName(), ['get', 'has'], true); + return in_array($methodReflection->getName(), ['get', 'has', 'createForm'], true); } public function getTypeFromMethodCall( @@ -41,8 +44,34 @@ public function getTypeFromMethodCall( return Helper::getGetTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap); case 'has': return Helper::getHasTypeFromMethodCall($methodReflection, $methodCall, $scope, $this->serviceMap); + case 'createForm': + return $this->getCreateFormTypeFromMethodCall($methodReflection, $methodCall, $scope); } throw new \PHPStan\ShouldNotHappenException(); } + private function getCreateFormTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type + { + $returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (!isset($methodCall->args[0])) { + return $returnType; + } + + $types = TypeUtils::getConstantStrings($scope->getType($methodCall->args[0]->value)); + if (count($types) !== 1) { + return $returnType; + } + + $formType = new ObjectType($types[0]->getValue()); + if (!(new ObjectType('Symfony\Component\Form\FormTypeInterface'))->isSuperTypeOf($formType)->yes()) { + return $returnType; + } + + return $formType; + } + } diff --git a/tests/Symfony/data/ExampleFormType.php b/tests/Symfony/data/ExampleFormType.php new file mode 100644 index 00000000..5de277fc --- /dev/null +++ b/tests/Symfony/data/ExampleFormType.php @@ -0,0 +1,10 @@ +createMock(MethodReflection::class); $methodGet->expects(self::once())->method('getName')->willReturn('get'); + $methodHas = $this->createMock(MethodReflection::class); + $methodHas->expects(self::once())->method('getName')->willReturn('has'); + + $methodCreateForm = $this->createMock(MethodReflection::class); + $methodCreateForm->expects(self::once())->method('getName')->willReturn('createForm'); + $methodFoo = $this->createMock(MethodReflection::class); $methodFoo->expects(self::once())->method('getName')->willReturn('foo'); $extension = new ControllerDynamicReturnTypeExtension(new ServiceMap(__DIR__ . '/../../Symfony/data/container.xml')); self::assertTrue($extension->isMethodSupported($methodGet)); + self::assertTrue($extension->isMethodSupported($methodHas)); + self::assertTrue($extension->isMethodSupported($methodCreateForm)); self::assertFalse($extension->isMethodSupported($methodFoo)); } @@ -104,4 +114,43 @@ public function getTypeFromMethodCallProvider(): array ]; } + public function testCreateContainer(): void + { + $parametersAcceptorFound = $this->createMock(ParametersAcceptor::class); + $parametersAcceptorFound->expects(self::once())->method('getReturnType')->willReturn(new ObjectType('Symfony\Component\Form\FormInterface')); + + $methodReflection = $this->createMock(MethodReflection::class); + $methodReflection->expects(self::once())->method('getName')->willReturn('createForm'); + $methodReflection->expects(self::once())->method('getVariants')->willReturn([$parametersAcceptorFound]); + + $methodCall = new MethodCall($this->createMock(Expr::class), 'createForm', [new Arg(new String_('PHPStan\Symfony\ExampleFormType'))]); + + $scope = $this->createMock(Scope::class); + $scope->expects(self::once())->method('getType')->willReturn(new ConstantStringType('PHPStan\Symfony\ExampleFormType')); + + $thisClassReflection = $this->createMock(ClassReflection::class); + $thisClassReflection->expects(self::once())->method('getName')->willReturn('Symfony\Component\Form\FormInterface'); + + $thatClassReflection = $this->createMock(ClassReflection::class); + $thatClassReflection->expects(self::once())->method('getName')->willReturn('PHPStan\Symfony\ExampleFormType'); + $thatClassReflection->expects(self::once())->method('isSubclassOf')->willReturn(true); + + $broker = $this->createMock(Broker::class); + $broker->expects(self::at(0))->method('hasClass')->willReturn(true); + $broker->expects(self::at(1))->method('hasClass')->willReturn(true); + $broker->expects(self::at(2))->method('getClass')->willReturn($thisClassReflection); + $broker->expects(self::at(3))->method('getClass')->willReturn($thatClassReflection); + Broker::registerInstance($broker); + + $extension = new ControllerDynamicReturnTypeExtension(new ServiceMap(__DIR__ . '/../../Symfony/data/container.xml')); + $type = $extension->getTypeFromMethodCall( + $methodReflection, + $methodCall, + $scope + ); + + self::assertInstanceOf(ObjectType::class, $type); + self::assertSame('PHPStan\Symfony\ExampleFormType', $type->getClassName()); + } + }