Skip to content

Commit

Permalink
Return type extension for Controller::createForm
Browse files Browse the repository at this point in the history
  • Loading branch information
lookyman committed Oct 23, 2018
1 parent d7d1624 commit 1b66d8e
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 2 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 30 additions & 1 deletion src/Type/Symfony/ControllerDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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(
Expand All @@ -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;
}

}
10 changes: 10 additions & 0 deletions tests/Symfony/data/ExampleFormType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Symfony;

use Symfony\Component\Form\AbstractType;

final class ExampleFormType extends AbstractType
{

}
49 changes: 49 additions & 0 deletions tests/Type/Symfony/ControllerDynamicReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Broker\Broker;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Symfony\ServiceMap;
Expand Down Expand Up @@ -39,11 +41,19 @@ public function testIsMethodSupported(): void
$methodGet = $this->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));
}

Expand Down Expand Up @@ -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());
}

}

0 comments on commit 1b66d8e

Please sign in to comment.