Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 84 Rules Overview
# 86 Rules Overview

## AbortIfRector

Expand Down Expand Up @@ -208,6 +208,27 @@ Add `parent::register();` call to `register()` class method in child of `Illumin

<br>

## AddUseAnnotationToHasFactoryTraitRector

Adds `@use` annotation to HasFactory trait usage to provide better IDE support.

:wrench: **configure it!**

- class: [`RectorLaravel\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector`](../src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php)

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

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

<br>

## AnonymousMigrationsRector

Convert migrations to anonymous classes.
Expand All @@ -229,13 +250,19 @@ Convert migrations to anonymous classes.

## AppEnvironmentComparisonToParameterRector

Replace `$app->environment() === 'local'` with `$app->environment('local')`
Replace app environment comparison with parameter or method call

- class: [`RectorLaravel\Rector\Expr\AppEnvironmentComparisonToParameterRector`](../src/Rector/Expr/AppEnvironmentComparisonToParameterRector.php)

```diff
-$app->environment() === 'production';
+$app->environment('production');
-$app->environment() === 'local';
-$app->environment() !== 'production';
-$app->environment() === 'testing';
-in_array($app->environment(), ['local', 'testing']);
+$app->isLocal();
+! $app->isProduction();
+$app->environment('testing');
+$app->environment(['local', 'testing']);
```

<br>
Expand Down Expand Up @@ -892,6 +919,32 @@ Changes middlewares from rule definitions from string to array notation.

<br>

## MakeModelAttributesAndScopesProtectedRector

Makes Model attributes and scopes protected

- class: [`RectorLaravel\Rector\ClassMethod\MakeModelAttributesAndScopesProtectedRector`](../src/Rector/ClassMethod/MakeModelAttributesAndScopesProtectedRector.php)

```diff
class User extends Model
{
- public function foo(): Attribute
+ protected function foo(): Attribute
{
return Attribute::get(fn () => $this->bar);
}

#[Scope]
- public function active(Builder $query): Builder
+ protected function active(Builder $query): Builder
{
return $query->where('active', true);
}
}
```

<br>

## MigrateToSimplifiedAttributeRector

Migrate to the new Model attributes syntax
Expand Down
285 changes: 285 additions & 0 deletions src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node\Stmt\TraitUse;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ObjectType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\ValueObject\Type\FullyQualifiedIdentifierTypeNode;
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use RectorLaravel\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;

/**
* @see \RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\AddUseAnnotationToHasFactoryTraitRectorTest
*/
final class AddUseAnnotationToHasFactoryTraitRector extends AbstractRector implements ConfigurableRectorInterface
{
public const string FACTORY_NAMESPACES = 'factoryNamespaces';

private const string USE_TAG_NAME = '@use';

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

/**
* @var string[]
*/
private array $factoryNamespaces = ['Database\\Factories'];

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 ConfiguredCodeSample(
<<<'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 \Illuminate\Database\Eloquent\Factories\HasFactory<\Database\Factories\UserFactory> */
use HasFactory;
}
CODE_SAMPLE,
[self::FACTORY_NAMESPACES => ['Database\\Factories']]
)]
);
}

public function configure(array $configuration): void
{
if ($configuration === []) {
$this->factoryNamespaces = ['Database\\Factories'];

return;
}

Assert::keyExists($configuration, self::FACTORY_NAMESPACES);
Assert::isArray($configuration[self::FACTORY_NAMESPACES]);
Assert::allString($configuration[self::FACTORY_NAMESPACES]);
$this->factoryNamespaces = $configuration[self::FACTORY_NAMESPACES];
}

/**
* @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) {
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;
}

$phpDocTagNode = new PhpDocTagNode(
self::USE_TAG_NAME,
new UsesTagValueNode(
new GenericTypeNode(
new FullyQualifiedIdentifierTypeNode(self::HAS_FACTORY_TRAIT),
[new FullyQualifiedIdentifierTypeNode($factoryClassName)]
),
''
)
);

$phpDocInfo->addPhpDocTagNode($phpDocTagNode);

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

return true;
}

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

$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?

}

private function getFactoryFromProperty(Class_ $class): ?string
{
foreach ($class->stmts as $stmt) {
if (! $stmt instanceof Property) {
continue;
}

if (! $this->isName($stmt, 'factory')) {
continue;
}

if ($stmt->props[0]->default === null) {
continue;
}

$defaultValue = $stmt->props[0]->default;

if ($defaultValue instanceof ClassConstFetch) {
$factoryClassName = $this->getName($defaultValue->class);
if ($factoryClassName !== null && $this->reflectionProvider->hasClass($factoryClassName)) {
return $factoryClassName;
}
}

if ($defaultValue instanceof String_) {
$factoryClassName = $defaultValue->value;
if ($this->reflectionProvider->hasClass($factoryClassName)) {
return $factoryClassName;
}
}
}

return null;
}

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

foreach ($this->factoryNamespaces as $factoryNamespace) {
// Remove leading backslash if present
$factoryNamespace = ltrim($factoryNamespace, '\\');

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

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

$factoryClassNames[] = '\\' . $factoryNamespace . '\\' . $deepNamespace . '\\' . $factoryName;
}
} elseif (str_contains($modelNamespace, 'App\\')) {
$appPosition = strpos($modelNamespace, 'App\\');
if ($appPosition === false) {
continue;
}
$afterApp = substr($modelNamespace, $appPosition + 4);

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

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

$factoryClassNames[] = '\\' . $factoryNamespace . '\\' . $factoryName;
}

return array_unique($factoryClassNames);
}
}
Loading