Skip to content
198 changes: 198 additions & 0 deletions src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\TraitUse;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ObjectType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
use RectorLaravel\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\AddUseAnnotationToHasFactoryTraitRectorTest
*/
final class AddUseAnnotationToHasFactoryTraitRector extends AbstractRector
{
private const string USE_TAG_NAME = '@use';

private const string HAS_FACTORY_TRAIT = 'Illuminate\Database\Eloquent\Factories\HasFactory';

public function __construct(
private readonly DocBlockUpdater $docBlockUpdater,
private readonly PhpDocInfoFactory $phpDocInfoFactory,
private readonly ReflectionProvider $reflectionProvider,
) {}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Adds @use annotation to HasFactory trait usage to provide better IDE support.',
[new CodeSample(
<<<'CODE_SAMPLE'
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
use HasFactory;
}
CODE_SAMPLE,
<<<'CODE_SAMPLE'
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory;
}
CODE_SAMPLE
)]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isObjectType($node, new ObjectType('Illuminate\Database\Eloquent\Model'))) {
return null;
}

$hasChanged = false;

foreach ($node->stmts as $stmt) {
if (! $stmt instanceof TraitUse) {
continue;
}

if (! $this->hasHasFactoryTrait($stmt)) {
continue;
}

if ($this->addUsePhpDocTag($stmt, $node)) {
$hasChanged = true;
}
}

if ($hasChanged) {
return $node;
}

return null;
}

private function hasHasFactoryTrait(TraitUse $traitUse): bool
{
foreach ($traitUse->traits as $trait) {
$traitName = $this->getName($trait);
if ($traitName === self::HAS_FACTORY_TRAIT || $traitName === 'HasFactory') {
return true;
}
}

return false;
}

private function addUsePhpDocTag(TraitUse $traitUse, Class_ $class): bool
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($traitUse);

if ($phpDocInfo->hasByName(self::USE_TAG_NAME)) {
return false;
}

$factoryClassName = $this->resolveFactoryClassName($class);
if ($factoryClassName === null) {
return false;
}

$useAnnotationValue = 'HasFactory<' . $factoryClassName . '>';

$phpDocTagNode = new PhpDocTagNode(
self::USE_TAG_NAME,
new GenericTagValueNode($useAnnotationValue)
);

$phpDocInfo->addPhpDocTagNode($phpDocTagNode);

$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($traitUse);

return true;
}

private function resolveFactoryClassName(Class_ $class): ?string
{
$className = $this->getName($class);
if ($className === null) {
return null;
}

$modelName = $this->nodeNameResolver->getShortName($className);

$factoryName = $modelName . 'Factory';

$currentNamespace = $class->namespacedName?->toString() ?? $className;

$factoryClassNames = $this->getPotentialFactoryClassNames($currentNamespace, $factoryName);

foreach ($factoryClassNames as $factoryClassName) {
if ($this->reflectionProvider->hasClass($factoryClassName)) {
return $factoryClassName;
}
}

return null;
Comment on lines +187 to +201
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this a lot, and even remove the subsequent getPotentialFactoryClassNames() method.
I think it's enough to replace in the $class->namespacedName->toString(), the App\Models or App part with Database\Factories.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of see what you mean but feel like this code is still needed to handle some edge cases after quickly playing with it. Happy to give it another shot if it's a deal breaker?

}

/**
* @return string[]
*/
private function getPotentialFactoryClassNames(string $modelNamespace, string $factoryName): array
{
$factoryClassNames = [];

if (str_contains($modelNamespace, '\\Models\\')) {
$afterModels = substr($modelNamespace, strpos($modelNamespace, '\\Models\\') + 8);

Check failure on line 174 in src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php

View workflow job for this annotation

GitHub Actions / PHPStan

Only numeric types are allowed in +, int<0, max>|false given on the left side.

if (str_contains($afterModels, '\\')) {
$namespaceParts = explode('\\', $afterModels);
array_pop($namespaceParts);
$deepNamespace = implode('\\', $namespaceParts);

$factoryClassNames[] = '\\Database\\Factories\\' . $deepNamespace . '\\' . $factoryName;
}
} elseif (str_contains($modelNamespace, 'App\\')) {
$afterApp = substr($modelNamespace, strpos($modelNamespace, 'App\\') + 4);

Check failure on line 184 in src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php

View workflow job for this annotation

GitHub Actions / PHPStan

Only numeric types are allowed in +, int<0, max>|false given on the left side.

if (str_contains($afterApp, '\\')) {
$namespaceParts = explode('\\', $afterApp);
array_pop($namespaceParts);
$deepNamespace = implode('\\', $namespaceParts);

$factoryClassNames[] = '\\Database\\Factories\\' . $deepNamespace . '\\' . $factoryName;
}
}
$factoryClassNames[] = '\\Database\\Factories\\' . $factoryName;

return array_unique($factoryClassNames);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class AddUseAnnotationToHasFactoryTraitRectorTest extends AbstractRectorTestCase
{
protected function setUp(): void
{
parent::setUp();

require_once __DIR__ . '/Factories/ProductFactory.php';
require_once __DIR__ . '/Factories/UserFactory.php';
require_once __DIR__ . '/Factories/Tenant/UserFactory.php';
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

/**
* @test
*/
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ProductFactory extends Factory
{
public function definition()

Check failure on line 9 in tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Factories/ProductFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method Database\Factories\ProductFactory::definition() has no return type specified.
{
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Database\Factories\Tenant;

use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
public function definition()

Check failure on line 9 in tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Factories/Tenant/UserFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method Database\Factories\Tenant\UserFactory::definition() has no return type specified.
{
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
public function definition()

Check failure on line 9 in tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Factories/UserFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan

Method Database\Factories\UserFactory::definition() has no return type specified.
{
return [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Models\Tenant;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
use HasFactory;
}

?>
-----
<?php

namespace App\Models\Tenant;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
/**
* @use HasFactory<\Database\Factories\Tenant\UserFactory>
*/
use HasFactory;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
use HasFactory;
}

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
/**
* @use HasFactory<\Database\Factories\UserFactory>
*/
use HasFactory;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Product extends Model
{
use HasFactory;
}

?>
-----
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Product extends Model
{
/**
* @use HasFactory<\Database\Factories\ProductFactory>
*/
use HasFactory;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class User extends Model
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory;
}

?>
Loading
Loading