diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index bef70236d..c5f6373aa 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 84 Rules Overview +# 86 Rules Overview ## AbortIfRector @@ -208,6 +208,27 @@ Add `parent::register();` call to `register()` class method in child of `Illumin
+## 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; + } +``` + +
+ ## AnonymousMigrationsRector Convert migrations to anonymous classes. @@ -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']); ```
@@ -892,6 +919,32 @@ Changes middlewares from rule definitions from string to array notation.
+## 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); + } + } +``` + +
+ ## MigrateToSimplifiedAttributeRector Migrate to the new Model attributes syntax diff --git a/src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php b/src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php new file mode 100644 index 000000000..a453b1277 --- /dev/null +++ b/src/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector.php @@ -0,0 +1,285 @@ + */ + 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> + */ + 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; + } + + 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); + } +} diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/AddUseAnnotationToHasFactoryTraitRectorTest.php b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/AddUseAnnotationToHasFactoryTraitRectorTest.php new file mode 100644 index 000000000..a199b7110 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/AddUseAnnotationToHasFactoryTraitRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/deep_namespace.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/deep_namespace.php.inc new file mode 100644 index 000000000..955b14aa0 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/deep_namespace.php.inc @@ -0,0 +1,30 @@ + +----- + + */ + use HasFactory; +} + +?> diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/fixture.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/fixture.php.inc new file mode 100644 index 000000000..4fbdbce58 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/fixture.php.inc @@ -0,0 +1,30 @@ + +----- + + */ + use HasFactory; +} + +?> diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/models_namespace.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/models_namespace.php.inc new file mode 100644 index 000000000..bc7b459c1 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/models_namespace.php.inc @@ -0,0 +1,30 @@ + +----- + + */ + use HasFactory; +} + +?> diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_existing_annotation.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_existing_annotation.php.inc new file mode 100644 index 000000000..14f152d6b --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_existing_annotation.php.inc @@ -0,0 +1,14 @@ + */ + use HasFactory; +} + +?> diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_factory_doesnt_exist.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_factory_doesnt_exist.php.inc new file mode 100644 index 000000000..6b03e8918 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_factory_doesnt_exist.php.inc @@ -0,0 +1,13 @@ + diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_non_model.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_non_model.php.inc new file mode 100644 index 000000000..7bba133a5 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/skip_non_model.php.inc @@ -0,0 +1,12 @@ + diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/with_factory_property.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/with_factory_property.php.inc new file mode 100644 index 000000000..3e92f5fea --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/with_factory_property.php.inc @@ -0,0 +1,34 @@ + +----- + + */ + use HasFactory; + + protected static string $factory = \RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Source\ProductFactory::class; +} + +?> diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/with_factory_property_string.php.inc b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/with_factory_property_string.php.inc new file mode 100644 index 000000000..b155ffa1b --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Fixture/with_factory_property_string.php.inc @@ -0,0 +1,34 @@ + +----- + + */ + use HasFactory; + + protected static string $factory = 'RectorLaravel\Tests\Rector\Class_\AddUseAnnotationToHasFactoryTraitRector\Source\ProductFactory'; +} + +?> diff --git a/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Source/ProductFactory.php b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Source/ProductFactory.php new file mode 100644 index 000000000..4683f57a1 --- /dev/null +++ b/tests/Rector/Class_/AddUseAnnotationToHasFactoryTraitRector/Source/ProductFactory.php @@ -0,0 +1,13 @@ +ruleWithConfiguration(AddUseAnnotationToHasFactoryTraitRector::class, [ + AddUseAnnotationToHasFactoryTraitRector::FACTORY_NAMESPACES => [ + 'RectorLaravel\\Tests\\Rector\\Class_\\AddUseAnnotationToHasFactoryTraitRector\\Source', + ], + ]); +};