Skip to content

Commit

Permalink
feat: support newFactory method when resolving factory
Browse files Browse the repository at this point in the history
  • Loading branch information
calebdw committed Aug 1, 2024
1 parent 3ab8326 commit 544f963
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,33 @@
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;

use function basename;
use function array_map;
use function class_exists;
use function count;
use function ltrim;
use function str_replace;

final class ModelFactoryDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
public function __construct(
private ReflectionProvider $reflectionProvider,
) {
}

public function getClass(): string
{
return Model::class;
Expand All @@ -47,9 +54,9 @@ public function getTypeFromStaticMethodCall(
): Type {
$class = $methodCall->class;

if (! $class instanceof Name) {
return new ErrorType();
}
$calledOnType = $class instanceof Name
? new ObjectType($scope->resolveName($class))
: $scope->getType($class);

if (count($methodCall->getArgs()) === 0) {
$isSingleModel = TrinaryLogic::createYes();
Expand All @@ -68,18 +75,66 @@ public function getTypeFromStaticMethodCall(
$isSingleModel = (new UnionType($numericTypes))->isSuperTypeOf($argType)->negate();
}

$factoryName = Factory::resolveFactoryName(ltrim($class->toCodeString(), '\\')); // @phpstan-ignore-line
return TypeCombinator::union(...array_map(
function (ClassReflection $classReflection) use ($scope, $isSingleModel) {
$factoryReflection = $this->getFactoryReflection($classReflection, $scope);

if ($factoryReflection === null) {
return new ErrorType();
}

return new ModelFactoryType($factoryReflection->getName(), null, $factoryReflection, $isSingleModel);
},
$calledOnType->getObjectClassReflections(),
));
}

private function getFactoryReflection(
ClassReflection $modelReflection,
Scope $scope,
): ClassReflection|null {
$factoryReflection = $this->getFactoryFromNewFactoryMethod($modelReflection, $scope);

if ($factoryReflection !== null) {
return $factoryReflection;
}

/** @phpstan-ignore argument.type (guaranteed to be model class-string) */
$factoryClass = Factory::resolveFactoryName($modelReflection->getName());

if (class_exists($factoryName)) {
return new ModelFactoryType($factoryName, null, null, $isSingleModel);
if (class_exists($factoryClass)) {
return $this->reflectionProvider->getClass($factoryClass);
}

$modelName = basename(str_replace('\\', '/', $class->toCodeString()));
return null;
}

private function getFactoryFromNewFactoryMethod(
ClassReflection $modelReflection,
Scope $scope,
): ClassReflection|null {
if (! $modelReflection->hasMethod('newFactory')) {
return null;
}

$factoryReflections = $modelReflection->getMethod('newFactory', $scope)
->getVariants()[0]
->getReturnType()
->getObjectClassReflections();

if (count($factoryReflections) !== 1) {
return null;
}

if (! class_exists('Database\\Factories\\' . $modelName . 'Factory')) {
return new ErrorType();
foreach ($factoryReflections as $factoryReflection) {
if (
$factoryReflection->isSubclassOf(Factory::class)
&& ! $factoryReflection->isAbstract()
) {
return $factoryReflection;
}
}

return new ModelFactoryType('Database\\Factories\\' . $modelName . 'Factory', null, null, $isSingleModel);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Unit;

use App\User;
use Generator;
use Larastan\Larastan\ReturnTypes\ModelFactoryDynamicStaticMethodReturnTypeExtension;
use Larastan\Larastan\Types\Factory\ModelFactoryType;
Expand All @@ -12,7 +13,7 @@
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\Dummy\DummyMethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
Expand All @@ -31,15 +32,30 @@

class ModelFactoryDynamicStaticMethodReturnTypeExtensionTest extends PHPStanTestCase
{
private ReflectionProvider $reflectionProvider;

public function setUp(): void
{
parent::setUp();

$this->reflectionProvider = $this->createReflectionProvider();
}

/** @test */
public function it_sets_the_is_single_model_flag_to_true_if_no_args_given(): void
{
$scope = $this->createMock(Scope::class);
$extension = new ModelFactoryDynamicStaticMethodReturnTypeExtension();
$class = new Name(User::class);

$scope = $this->createMock(Scope::class);
$scope->method('resolveName')->with($class)->willReturn(User::class);

$extension = new ModelFactoryDynamicStaticMethodReturnTypeExtension(
$this->createReflectionProvider(),
);

$type = $extension->getTypeFromStaticMethodCall(
new DummyMethodReflection('factory'), // @phpstan-ignore-line
new StaticCall(new Name('App\\User'), 'factory', []),
$this->reflectionProvider->getClass(User::class)->getNativeMethod('factory'),
new StaticCall($class, 'factory', []),
$scope,
);

Expand All @@ -53,14 +69,19 @@ public function it_sets_the_is_single_model_flag_to_true_if_no_args_given(): voi
*/
public function it_sets_the_is_single_model_flag_correctly(Type $phpstanType, TrinaryLogic $expected): void
{
$scope = $this->createMock(Scope::class);
$extension = new ModelFactoryDynamicStaticMethodReturnTypeExtension();
$class = new Name(User::class);

$scope = $this->createMock(Scope::class);
$scope->method('resolveName')->with($class)->willReturn(User::class);
$scope->method('getType')->willReturn($phpstanType);

$extension = new ModelFactoryDynamicStaticMethodReturnTypeExtension(
$this->createReflectionProvider(),
);

$type = $extension->getTypeFromStaticMethodCall(
new DummyMethodReflection('factory'), // @phpstan-ignore-line
new StaticCall(new Name('App\\User'), 'factory', [new Arg(new LNumber(1))]), // args doesn't matter
$this->reflectionProvider->getClass(User::class)->getNativeMethod('factory'),
new StaticCall($class, 'factory', [new Arg(new LNumber(1))]), // args doesn't matter
$scope,
);

Expand Down

0 comments on commit 544f963

Please sign in to comment.