Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
devfrey committed Jul 9, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
0 parents commit 852f0b8
Showing 26 changed files with 8,630 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
* text=auto

.github/ export-ignore
tests/ export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
phpunit.xml.dist export-ignore
phpstan.neon.dist export-ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor/
.phpunit.result.cache
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## Installation

```bash
composer require --dev devfrey/rector-eloquent-generics
```

```php
return RectorConfig::configure()
// ...
->withRules([
Devfrey\RectorLaravel\Eloquent\AddBuilderPropertyRector::class,
Devfrey\RectorLaravel\Eloquent\AddGenericHasBuilderTraitRector::class,
Devfrey\RectorLaravel\Eloquent\DocumentRelationGenericsRector::class,
]);
```
41 changes: 41 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "devfrey/rector-eloquent-generics",
"description": "Small set of Rector rules for Eloquent's new generics.",
"license": "MIT",
"type": "library",
"authors": [
{
"name": "Jeffrey Angenent",
"email": "[email protected]"
}
],
"require": {
"php": "^8.2",
"rector/rector": "^1.2",
"symplify/rule-doc-generator-contracts": "^11.2"
},
"require-dev": {
"laravel/framework": "^11.15.0",
"phpstan/extension-installer": "^1.4.1",
"phpstan/phpstan": "^1.11.7",
"phpunit/phpunit": "^11.2.6",
"symplify/phpstan-rules": "^13.0"
},
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"Devfrey\\RectorLaravel\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
},
"sort-packages": true
}
}
7,396 changes: 7,396 additions & 0 deletions composer.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
includes:
- vendor/symplify/phpstan-rules/config/rector-rules.neon

parameters:
level: 9
paths:
- src
- tests
18 changes: 18 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Tests">
<directory>./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>./src</directory>
</include>
</source>
</phpunit>
119 changes: 119 additions & 0 deletions src/Eloquent/AddBuilderPropertyRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Devfrey\RectorLaravel\Eloquent;

use PhpParser\Builder\Property;
use PhpParser\Node;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Tests\Eloquent\AddBuilderPropertyRector\AddBuilderPropertyRectorTest
*/
final class AddBuilderPropertyRector extends ModelRector
{
/**
* Get the rule definition for the Rector rule.
*
* @return \Symplify\RuleDocGenerator\ValueObject\RuleDefinition
*/
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Refactor newEloquentBuilder() to $builder', [
new CodeSample(
<<<'CODE_SAMPLE'
public function newEloquentBuilder($query): UserBuilder
{
return new UserBuilder($query);
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
protected static string $builder = UserBuilder::class;
CODE_SAMPLE,
),
]);
}

/**
* Refactor the Eloquent model.
*
* @param \PhpParser\Node\Stmt\Class_ $model
* @return \PhpParser\Node\Stmt\Class_|null
*/
public function refactorModel(Class_ $model): ?Class_
{
foreach ($model->stmts as $key => $stmt) {
if (!$stmt instanceof ClassMethod) {
continue;
}

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

if (!$stmt->getReturnType() instanceof Name\FullyQualified) {
// If the method does not have a native return type, we cannot
// determine the builder class and therefore cannot refactor
// the method to a property.
continue;
}

// Remove the newEloquentBuilder() method
unset($model->stmts[$key]);

// Add the $builder property
$this->insertAfterTraitUses(
$model,
$this->createProtectedStaticBuilderProperty($stmt->getReturnType()),
);

return $model;
}

return null;
}

/**
* Insert the property after the trait uses.
*
* @param \PhpParser\Node\Stmt\Class_ $class
* @param \PhpParser\Node\Stmt\Property $property
* @return void
*/
private function insertAfterTraitUses(Class_ $class, Node\Stmt\Property $property): void
{
$traitUseIndex = -1;

foreach ($class->stmts as $index => $stmt) {
if ($stmt instanceof Node\Stmt\TraitUse) {
$traitUseIndex = $index;
}
}

array_splice($class->stmts, $traitUseIndex + 1, 0, [$property]);
}

/**
* Create a protected static $builder property.
*
* @param \PhpParser\Node\Name\FullyQualified $builderClass
* @return \PhpParser\Node\Stmt\Property
*/
private function createProtectedStaticBuilderProperty(Name\FullyQualified $builderClass): Node\Stmt\Property
{
// \App\Models\Builders\UserBuilder::class
$default = $this->nodeFactory->createClassConstReference($builderClass->toString());

// protected static $builder = \App\Models\Builders\UserBuilder::class
return (new Property('builder'))
->makeProtected()
->makeStatic()
->setType('string')
->setDefault($default)
->getNode();
}
}
149 changes: 149 additions & 0 deletions src/Eloquent/AddGenericHasBuilderTraitRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

namespace Devfrey\RectorLaravel\Eloquent;

use PhpParser\Comment\Doc;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\TraitUse;
use Rector\NodeManipulator\ClassManipulator;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Tests\Eloquent\AddGenericHasBuilderTraitRector\AddGenericHasBuilderTraitRectorTest
*/
final class AddGenericHasBuilderTraitRector extends ModelRector
{
/**
* Create a new Rector rule instance.
*
* @param \Rector\NodeManipulator\ClassManipulator $classManipulator
* @return void
*/
public function __construct(
private readonly ClassManipulator $classManipulator,
) {
}

/**
* Get the rule definition for the Rector rule.
*
* @return \Symplify\RuleDocGenerator\ValueObject\RuleDefinition
*/
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('// @todo fill the description', [
new CodeSample(
<<<'CODE_SAMPLE'
// @todo fill code before
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
// @todo fill code after
CODE_SAMPLE,
),
]);
}

/**
* Refactor the Eloquent model.
*
* @param \PhpParser\Node\Stmt\Class_ $model
* @return \PhpParser\Node\Stmt\Class_|null
*/
public function refactorModel(Class_ $model): ?Class_
{
if ($this->classManipulator->hasTrait($model, 'Illuminate\Database\Eloquent\HasBuilder')) {
return null;
}

$builderClassName = $this->detectBuilderClassName($model);

if (is_null($builderClassName)) {
return null;
}

$model->stmts = [
$this->createTraitUse($builderClassName),
...$model->stmts,
];

return $model;
}

/**
* Detect the given model's builder name.
*
* @param \PhpParser\Node\Stmt\Class_ $model
* @return \PhpParser\Node\Name\FullyQualified|null
*/
private function detectBuilderClassName(Class_ $model): ?FullyQualified
{
// First we check if the model has a newEloquentBuilder() method
$newEloquentBuilder = $model->getMethod('newEloquentBuilder');

if (!is_null($newEloquentBuilder)) {
$returnType = $newEloquentBuilder->getReturnType();

if ($returnType instanceof FullyQualified) {
return $returnType;
}

// If the return type cannot be determined, we immediately abort
// and skip looking for the $builder property. Eloquent only uses
// the $builder property if the newEloquentBuilder() method
// is missing.
return null;
}

// Otherwise, we check if the model has a protected static $builder property
foreach ($model->getProperties() as $property) {
if (
!$property->isProtected()
|| !$property->isStatic()
|| !$this->isName($property, 'builder')
) {
continue;
}

$default = $property->props[0]->default;

if (
$default instanceof ClassConstFetch
&& $default->class instanceof FullyQualified
) {
return $default->class;
}

// If the $builder property is not a class constant fetch, we
// cannot determine the builder type.
return null;
}

return null;
}

/**
* Create a new trait use statement.
*
* @param \PhpParser\Node\Name\FullyQualified $builderClassName
* @return \PhpParser\Node\Stmt\TraitUse
*/
private function createTraitUse(FullyQualified $builderClassName): TraitUse
{
$traitUse = new TraitUse([new FullyQualified('Illuminate\Database\Eloquent\HasBuilder')]);
$traitUse->setDocComment(
new Doc(
<<<PHPDOC
/**
* @use \Illuminate\Database\Eloquent\HasBuilder<{$builderClassName->toCodeString()}<\$this>>
*/
PHPDOC,
),
);

return $traitUse;
}
}
278 changes: 278 additions & 0 deletions src/Eloquent/DocumentRelationGenericsRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
<?php

namespace Devfrey\RectorLaravel\Eloquent;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\Exception\ShouldNotHappenException;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedGenericObjectType;
use Rector\StaticTypeMapper\ValueObject\Type\SelfStaticType;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

final class DocumentRelationGenericsRector extends ModelRector
{
private const SIMPLE_RELATIONS = [
'belongsTo' => 'Illuminate\Database\Eloquent\Relations\BelongsTo',
'belongsToMany' => 'Illuminate\Database\Eloquent\Relations\BelongsToMany',
'hasMany' => 'Illuminate\Database\Eloquent\Relations\HasMany',
'hasOne' => 'Illuminate\Database\Eloquent\Relations\HasOne',
'morphMany' => 'Illuminate\Database\Eloquent\Relations\MorphMany',
'morphOne' => 'Illuminate\Database\Eloquent\Relations\MorphOne',
// 'morphTo' => 'Illuminate\Database\Eloquent\Relations\MorphTo',
'morphToMany' => 'Illuminate\Database\Eloquent\Relations\MorphToMany',
];

private const INTERMEDIATE_RELATIONS = [
'hasManyThrough' => 'Illuminate\Database\Eloquent\Relations\HasManyThrough',
'hasOneThrough' => 'Illuminate\Database\Eloquent\Relations\HasOneThrough',
];

/**
* Create a new Rector rule instance.
*
* @param \Rector\PhpParser\Node\BetterNodeFinder $betterNodeFinder
* @param \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory $phpDocInfoFactory
* @param \Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger $phpDocTypeChanger
* @return void
*/
public function __construct(
private readonly BetterNodeFinder $betterNodeFinder,
private readonly PhpDocInfoFactory $phpDocInfoFactory,
private readonly PhpDocTypeChanger $phpDocTypeChanger,
) {
}

/**
* Get the rule definition for the Rector rule.
*
* @return \Symplify\RuleDocGenerator\ValueObject\RuleDefinition
*/
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('// @todo fill the description', [
new CodeSample(
<<<'CODE_SAMPLE'
// @todo fill code before
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
// @todo fill code after
CODE_SAMPLE,
),
]);
}

/**
* Refactor the Eloquent model.
*
* @param \PhpParser\Node\Stmt\Class_ $model
* @return \PhpParser\Node\Stmt\Class_|null
*/
public function refactorModel(Class_ $model): ?Class_
{
$modelType = $this->nodeTypeResolver->getType($model);

if (!$modelType instanceof TypeWithClassName) {
throw new ShouldNotHappenException();
}

$hasChanged = false;

foreach ($model->getMethods() as $method) {
$returns = $this->betterNodeFinder->findInstancesOfInFunctionLikeScoped($method, Return_::class);

if (count($returns) !== 1) {
continue;
}

$returnExpression = $returns[0]->expr;

if (!$returnExpression instanceof MethodCall) {
continue;
}

/// If the method call is chained, we take the first method call
$methodCall = $this->getParentMethodCall($returnExpression);

$genericRelationType = $this->parseMethodCallIntoGenericObjectTypeIfRelation($modelType, $methodCall);

if (is_null($genericRelationType)) {
continue;
}

$hasChanged = $this->phpDocTypeChanger->changeReturnType(
functionLike: $method,
phpDocInfo: $this->phpDocInfoFactory->createFromNodeOrEmpty($method),
newType: $genericRelationType,
) || $hasChanged;
}

// Return the node if changes were made to avoid unnecessary re-parsing
// of the node, which also causes issues.
return $hasChanged ? $model : null;
}

/**
* Parse a method call into a generic object type if it is a relation.
*
* @param \PhpParser\Node\Expr\MethodCall $methodCall
* @return \Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedGenericObjectType|null
*/
private function parseMethodCallIntoGenericObjectTypeIfRelation(
TypeWithClassName $modelType,
MethodCall $methodCall,
): ?FullyQualifiedGenericObjectType {
$selfType = new SelfStaticType(
$modelType->getClassReflection() ?? throw new ShouldNotHappenException(),
);

foreach (self::SIMPLE_RELATIONS as $relationMethod => $relationClass) {
if (!$this->isName($methodCall->name, $relationMethod)) {
continue;
}

$relatedType = $this->resolveFirstArgumentObjectType($methodCall);

if (is_null($relatedType)) {
return null;
}

return new FullyQualifiedGenericObjectType(
$relationClass,
[
$this->normalizeIntoStaticObjectType($relatedType),
$selfType,
],
);
}

foreach (self::INTERMEDIATE_RELATIONS as $relationMethod => $relationClass) {
if (!$this->isName($methodCall, $relationMethod)) {
continue;
}

[$relatedType, $throughType] = $this->resolveFirstTwoArgumentObjectTypes($methodCall);

if (is_null($relatedType) || is_null($throughType)) {
return null;
}

return new FullyQualifiedGenericObjectType(
$relationClass,
[
$this->normalizeIntoStaticObjectType($relatedType),
$this->normalizeIntoStaticObjectType($throughType),
$selfType,
],
);
}

if ($this->isName($methodCall->name, 'morphTo')) {
return new FullyQualifiedGenericObjectType(
'Illuminate\Database\Eloquent\Relations\MorphTo',
[
new ObjectType('Illuminate\Database\Eloquent\Model'),
$selfType,
],
);
}

return null;
}

/**
* Normalize the given object type into a static object type.
*
* @template T of \PHPStan\Type\Type
* @param \PHPStan\Type\ObjectType $type
* @return ($type is \PHPStan\Type\ThisType ? \PHPStan\Type\ThisType : T)
*/
private function normalizeIntoStaticObjectType(Type $type): ObjectType
{
return $type instanceof ThisType ? $type->getStaticObjectType() : $type;
}

/**
* Resolve the first argument type from the given method call.
*
* @param \PhpParser\Node\Expr\MethodCall $methodCall
* @return \PHPStan\Type\ObjectType|null
*/
private function resolveFirstArgumentObjectType(MethodCall $methodCall)
{
$arguments = $methodCall->getArgs();

if ($arguments === []) {
return null;
}

return $this->resolveObjectTypeFromArgument($arguments[0]);
}

/**
* Resolve the first two argument types from the given method call.
*
* @param \PhpParser\Node\Expr\MethodCall $methodCall
* @return array{\PHPStan\Type\ObjectType|null, \PHPStan\Type\ObjectType|null}
*/
private function resolveFirstTwoArgumentObjectTypes(MethodCall $methodCall): array
{
$arguments = $methodCall->getArgs();

if (count($arguments) < 2) {
return [null, null];
}

$firstArgument = $this->resolveObjectTypeFromArgument($arguments[0]);
$secondArgument = $this->resolveObjectTypeFromArgument($arguments[1]);

return [$firstArgument, $secondArgument];
}

/**
* Resolve the object type from the given argument.
*
* @param \PhpParser\Node\Arg $argument
* @return \PHPStan\Type\ObjectType|null
*/
private function resolveObjectTypeFromArgument(Arg $argument): ?ObjectType
{
if (!$argument->value instanceof ClassConstFetch) {
return null;
}

$resolvedType = $this->nodeTypeResolver->getType($argument->value->class);

if (!$resolvedType instanceof ObjectType) {
return null;
}

return $resolvedType;
}

/**
* Get the parent method call of the given method call.
*
* @param \PhpParser\Node\Expr\MethodCall $methodCall
* @return \PhpParser\Node\Expr\MethodCall
*/
private function getParentMethodCall(MethodCall $methodCall): MethodCall
{
if ($methodCall->var instanceof MethodCall) {
return $this->getParentMethodCall($methodCall->var);
}

return $methodCall;
}
}
49 changes: 49 additions & 0 deletions src/Eloquent/ModelRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Devfrey\RectorLaravel\Eloquent;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PHPStan\Type\ObjectType;
use Rector\Exception\ShouldNotHappenException;
use Rector\Rector\AbstractRector;

abstract class ModelRector extends AbstractRector
{
/**
* Get the node types that the Rector rule refactors.
*
* @return array<class-string<\PhpParser\Node>>
*/
final public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* Refactor the Eloquent model.
*
* @param \PhpParser\Node\Stmt\Class_ $model
* @return \PhpParser\Node\Stmt\Class_|null
*/
abstract public function refactorModel(Class_ $model): ?Class_;

/**
* Refactor the node.
*
* @param \PhpParser\Node $node
* @return \PhpParser\Node\Stmt\Class_|null
*/
final public function refactor(Node $node): ?Class_
{
if (!$node instanceof Class_) {
throw new ShouldNotHappenException();
}

if (!$this->isObjectType($node, new ObjectType('\Illuminate\Database\Eloquent\Model'))) {
return null;
}

return $this->refactorModel($node);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Tests\Eloquent\AddBuilderPropertyRector;

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

final class AddBuilderPropertyRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

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

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

namespace Tests\Eloquent\AddBuilderPropertyRector\Fixture;

use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model
{
use HasFactory;
use SoftDeletes;

protected $fillable = ['email'];

public function foo(): BelongsTo
{
return $this->belongsTo(Foo::class);
}

public function newEloquentBuilder($query): UserBuilder
{
return new UserBuilder($query);
}
}

?>
-----
<?php

namespace Tests\Eloquent\AddBuilderPropertyRector\Fixture;

use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model
{
use HasFactory;
use SoftDeletes;
protected static string $builder = \App\Models\Builders\UserBuilder::class;

protected $fillable = ['email'];

public function foo(): BelongsTo
{
return $this->belongsTo(Foo::class);
}
}

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

namespace Tests\Eloquent\AddBuilderPropertyRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class User extends Model
{
public function foo(): BelongsTo
{
return $this->belongsTo(Foo::class);
}
}

?>
-----
<?php

namespace Tests\Eloquent\AddBuilderPropertyRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class User extends Model
{
public function foo(): BelongsTo
{
return $this->belongsTo(Foo::class);
}
}

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

namespace Tests\Eloquent\AddBuilderPropertyRector\Fixture;

use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
public function newEloquentBuilder($query)
{
return new UserBuilder($query);
}
}

?>
-----
<?php

namespace Tests\Eloquent\AddBuilderPropertyRector\Fixture;

use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
public function newEloquentBuilder($query)
{
return new UserBuilder($query);
}
}

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

use Devfrey\RectorLaravel\Eloquent\AddBuilderPropertyRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig) {
$rectorConfig->rule(AddBuilderPropertyRector::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector;

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

final class AddGenericHasBuilderTraitRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

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

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

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use App\Models\Builders\FooBuilder;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
protected $table = 'comments';

protected static string $builder = FooBuilder::class;
}

?>
-----
<?php

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use App\Models\Builders\FooBuilder;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
/**
* @use \Illuminate\Database\Eloquent\HasBuilder<\App\Models\Builders\FooBuilder<$this>>
*/
use \Illuminate\Database\Eloquent\HasBuilder;
protected $table = 'comments';

protected static string $builder = FooBuilder::class;
}

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

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use App\Models\Builders\FooBuilder;
use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
protected static string $builder = FooBuilder::class;

public function newEloquentBuilder($query): UserBuilder
{
return new UserBuilder($query);
}
}

?>
-----
<?php

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use App\Models\Builders\FooBuilder;
use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* @use \Illuminate\Database\Eloquent\HasBuilder<\App\Models\Builders\UserBuilder<$this>>
*/
use \Illuminate\Database\Eloquent\HasBuilder;
protected static string $builder = FooBuilder::class;

public function newEloquentBuilder($query): UserBuilder
{
return new UserBuilder($query);
}
}

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

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use Illuminate\Database\Eloquent\HasBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
/**
* @use \Illuminate\Database\Eloquent\HasBuilder<\App\Models\Builders\PostBuilder>
*/
use HasBuilder;
use SoftDeletes;
}

?>
-----
<?php

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use Illuminate\Database\Eloquent\HasBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
/**
* @use \Illuminate\Database\Eloquent\HasBuilder<\App\Models\Builders\PostBuilder>
*/
use HasBuilder;
use SoftDeletes;
}

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

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
public function newEloquentBuilder($query)
{
return new UserBuilder($query);
}
}

?>
-----
<?php

namespace Tests\Eloquent\AddGenericHasBuilderTraitRector\Fixture;

use App\Models\Builders\UserBuilder;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
public function newEloquentBuilder($query)
{
return new UserBuilder($query);
}
}

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

use Devfrey\RectorLaravel\Eloquent\AddGenericHasBuilderTraitRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig) {
$rectorConfig->rule(AddGenericHasBuilderTraitRector::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Eloquent\DocumentRelationGenericsRector;

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

final class DocumentRelationGenericsRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

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

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
160 changes: 160 additions & 0 deletions tests/Eloquent/DocumentRelationGenericsRector/Fixture/model.php.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

namespace Tests\Eloquent\DocumentRelationGenericsRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\SoftDeletingScope;

class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}

/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function followers()
{
return $this->hasMany(self::class, 'followers');
}

public function teams()
{
return $this->belongsToMany(Team::class)
->using(UserTeam::class)
->as('user_team')
->withPivot([
'role',
]);
}
}

class Post extends Model
{
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withoutGlobalScope(SoftDeletingScope::class);
}

public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}

class Image extends Model
{
/**
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function imageable()
{
return $this->morphTo();
}
}

class Team extends Model
{
//
}

class UserTeam extends Pivot
{
//
}

?>
-----
<?php

namespace Tests\Eloquent\DocumentRelationGenericsRector\Fixture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\SoftDeletingScope;

class User extends Model
{
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Tests\Eloquent\DocumentRelationGenericsRector\Fixture\Post, $this>
*/
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}

/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany<\Tests\Eloquent\DocumentRelationGenericsRector\Fixture\User, $this>
*/
public function followers()
{
return $this->hasMany(self::class, 'followers');
}

/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\Tests\Eloquent\DocumentRelationGenericsRector\Fixture\Team, $this>
*/
public function teams()
{
return $this->belongsToMany(Team::class)
->using(UserTeam::class)
->as('user_team')
->withPivot([
'role',
]);
}
}

class Post extends Model
{
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\Tests\Eloquent\DocumentRelationGenericsRector\Fixture\User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class)->withoutGlobalScope(SoftDeletingScope::class);
}

/**
* @return \Illuminate\Database\Eloquent\Relations\MorphOne<\Tests\Eloquent\DocumentRelationGenericsRector\Fixture\Image, $this>
*/
public function image(): MorphOne
{
return $this->morphOne(Image::class, 'imageable');
}
}

class Image extends Model
{
/**
* @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, $this>
*/
public function imageable()
{
return $this->morphTo();
}
}

class Team extends Model
{
//
}

class UserTeam extends Pivot
{
//
}

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

use Devfrey\RectorLaravel\Eloquent\DocumentRelationGenericsRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig) {
$rectorConfig->rule(DocumentRelationGenericsRector::class);
};

0 comments on commit 852f0b8

Please sign in to comment.