Skip to content

Commit

Permalink
Merge branch 'newFactory' into 2.x
Browse files Browse the repository at this point in the history
  • Loading branch information
calebdw committed Aug 1, 2024
2 parents a35e036 + 544f963 commit 1a75590
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand All @@ -19,13 +20,15 @@
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 array_map;
use function class_exists;
use function count;
use function ltrim;

final class ModelFactoryDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
Expand All @@ -51,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 @@ -72,61 +75,66 @@ public function getTypeFromStaticMethodCall(
$isSingleModel = (new UnionType($numericTypes))->isSuperTypeOf($argType)->negate();
}

$factoryClass = $this->getFactoryClass($class, $scope);
return TypeCombinator::union(...array_map(
function (ClassReflection $classReflection) use ($scope, $isSingleModel) {
$factoryReflection = $this->getFactoryReflection($classReflection, $scope);

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

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

private function getFactoryClass(Name $model, Scope $scope): string|null
{
$factoryClass = $this->getFactoryClassFromNewFactoryMethod($model, $scope);
private function getFactoryReflection(
ClassReflection $modelReflection,
Scope $scope,
): ClassReflection|null {
$factoryReflection = $this->getFactoryFromNewFactoryMethod($modelReflection, $scope);

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

/** @var class-string<Model> $className */
$className = ltrim($model->toCodeString(), '\\');
$factoryClass = Factory::resolveFactoryName($className);
/** @phpstan-ignore argument.type (guaranteed to be model class-string) */
$factoryClass = Factory::resolveFactoryName($modelReflection->getName());

if (class_exists($factoryClass)) {
return $factoryClass;
return $this->reflectionProvider->getClass($factoryClass);
}

return null;
}

private function getFactoryClassFromNewFactoryMethod(Name $model, Scope $scope): string|null
{
$modelReflection = $this->reflectionProvider->getClass($model->toString());

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

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

$factoryClasses = $returnType->getObjectClassNames();
->getReturnType()
->getObjectClassReflections();

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

$factoryReflection = $this->reflectionProvider->getClass($factoryClasses[0]);

if (
! $factoryReflection->isSubclassOf(Factory::class)
|| $factoryReflection->isAbstract()
) {
return null;
foreach ($factoryReflections as $factoryReflection) {
if (
$factoryReflection->isSubclassOf(Factory::class)
&& ! $factoryReflection->isAbstract()
) {
return $factoryReflection;
}
}

return $factoryReflection->getName();
return null;
}
}
28 changes: 28 additions & 0 deletions tests/Type/data/model-factories.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
use App\Post;
use App\User;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use function PHPStan\Testing\assertType;

function test(?int $foo): void {
Expand Down Expand Up @@ -75,3 +78,28 @@ function test(?int $foo): void {
assertType('Database\Factories\UserFactory', User::factory()->trashed());
assertType('*ERROR*', Post::factory()->trashed());
}

class Comment extends Model
{
use HasFactory;

/** @return CommentFactory */
protected static function newFactory(): Factory
{
return CommentFactory::new();
}

private function test(): void
{
assertType('ModelFactories\CommentFactory', static::factory());
}
}

/** @extends Factory<Comment> */
class CommentFactory extends Factory
{
public function definition(): array
{
return [];
}
}
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,18 +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);
$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(
/** @phpstan-ignore phpstanApi.constructor (not covered by BC promise) */
new DummyMethodReflection('factory'),
new StaticCall(new Name('App\\User'), 'factory', []),
$this->reflectionProvider->getClass(User::class)->getNativeMethod('factory'),
new StaticCall($class, 'factory', []),
$scope,
);

Expand All @@ -56,17 +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);
$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(),
);

$scope->method('getType')->willReturn($phpstanType);

$type = $extension->getTypeFromStaticMethodCall(
/** @phpstan-ignore phpstanApi.constructor (not covered by BC promise) */
new DummyMethodReflection('factory'),
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 1a75590

Please sign in to comment.