Skip to content
Merged
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
63 changes: 63 additions & 0 deletions src/NodeAnalyzer/ScopeAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\NodeAnalyzer;

use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\ObjectType;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;

final readonly class ScopeAnalyzer
{
private const string SCOPE_ATTRIBUTE = 'Illuminate\Database\Eloquent\Attributes\Scope';

private const string ELOQUENT_BUILDER = 'Illuminate\Database\Eloquent\Builder';

public function __construct(
private NodeNameResolver $nodeNameResolver,
private NodeTypeResolver $nodeTypeResolver,
private PhpAttributeAnalyzer $phpAttributeAnalyzer,
) {}

/**
* Checks for the "scope" + uppercase char naming convention and a
* Builder-typed (or untyped) first parameter.
*/
public function isNamedScope(ClassMethod $classMethod): bool
{
$name = (string) $this->nodeNameResolver->getName($classMethod);

if (! (bool) preg_match('/^scope[A-Z].+$/', $name)) {
return false;
}

return $this->hasBuilderFirstParameter($classMethod);
}

/**
* Named scope OR #[Scope] attribute.
*/
public function isScopeMethod(ClassMethod $classMethod): bool
{
return $this->isNamedScope($classMethod)
|| $this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, self::SCOPE_ATTRIBUTE);
}

private function hasBuilderFirstParameter(ClassMethod $classMethod): bool
{
if ($classMethod->params === []) {
return false;
}

$firstParam = $classMethod->params[0];

if ($firstParam->type === null) {
return true;
}

return $this->nodeTypeResolver->isObjectType($firstParam->type, new ObjectType(self::ELOQUENT_BUILDER));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type\ObjectType;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\PHPStan\ScopeFetcher;
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
use RectorLaravel\AbstractRector;
use RectorLaravel\NodeAnalyzer\ScopeAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

Expand All @@ -23,7 +23,7 @@ class MakeModelAttributesAndScopesProtectedRector extends AbstractRector
{
public function __construct(
private readonly VisibilityManipulator $visibilityManipulator,
private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer
private readonly ScopeAnalyzer $scopeAnalyzer,
) {}

public function getRuleDefinition(): RuleDefinition
Expand Down Expand Up @@ -97,7 +97,7 @@ private function shouldSkipNode(ClassMethod $classMethod, Scope $scope): bool
return true;
}

if (! $this->isAttributeMethod($classMethod) && ! $this->isScopeMethod($classMethod)) {
if (! $this->isAttributeMethod($classMethod) && ! $this->scopeAnalyzer->isScopeMethod($classMethod)) {
return true;
}

Expand Down Expand Up @@ -133,15 +133,4 @@ private function isAttributeMethod(ClassMethod $classMethod): bool

return $this->isObjectType($classMethod->returnType, new ObjectType('Illuminate\Database\Eloquent\Casts\Attribute'));
}

private function isScopeMethod(ClassMethod $classMethod): bool
{
$name = $this->getName($classMethod);

if ((bool) preg_match('/^scope.+$/', $name)) {
return true;
}

return $this->phpAttributeAnalyzer->hasPhpAttribute($classMethod, 'Illuminate\Database\Eloquent\Attributes\Scope');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PHPStan\Type\ObjectType;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use RectorLaravel\AbstractRector;
use RectorLaravel\NodeAnalyzer\ScopeAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

Expand All @@ -28,6 +29,7 @@ final class ScopeNamedClassMethodToScopeAttributedClassMethodRector extends Abst
public function __construct(
private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer,
private readonly ReflectionProvider $reflectionProvider,
private readonly ScopeAnalyzer $scopeAnalyzer,
) {}

public function getRuleDefinition(): RuleDefinition
Expand Down Expand Up @@ -87,13 +89,12 @@ public function refactor(Node $node): ?Node
continue;
}

$name = $this->getName($classMethod);
// make sure it starts with scope and the next character is upper case
if (! str_starts_with($name, 'scope') || ! ctype_upper(substr($name, 5, 1))) {
if (! $this->scopeAnalyzer->isNamedScope($classMethod)) {
continue;
}

$newName = lcfirst(str_replace('scope', '', $name));
$name = $this->getName($classMethod);
$newName = lcfirst(substr($name, 5));

if ($classReflection->hasMethod($newName)) {
continue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace RectorLaravel\Tests\Rector\ClassMethod\MakeModelAttributesAndScopesProtectedRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class ProtectsScopeWithNonBuilderReturn extends Model
{
public function scopeString(Builder $query): string
{
return 'foo';
}
}

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\ClassMethod\MakeModelAttributesAndScopesProtectedRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class ProtectsScopeWithNonBuilderReturn extends Model
{
protected function scopeString(Builder $query): string
{
return 'foo';
}
}

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

namespace RectorLaravel\Tests\Rector\ClassMethod\MakeModelAttributesAndScopesProtectedRector\Fixture;

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

class SkipNonScopeSignatures extends Model
{
public function scopeParam(string $param): Builder
{
return $this->where('foo', true);
}

public function scopeVoid(): void {}

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

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

namespace RectorLaravel\Tests\Rector\ClassMethod\MakeModelAttributesAndScopesProtectedRector\Fixture;

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

class SkipRelationMethods extends Model
{
public function scopeTypes(): HasMany
{
return $this->hasMany(ScopeType::class);
}
}

?>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;

class SomeClass extends Model
{
public function scopeSomeMethod()
public function scopeSomeMethod($query)
{

}
Expand All @@ -23,7 +23,7 @@ use Illuminate\Database\Eloquent\Model;
class SomeClass extends Model
{
#[\Illuminate\Database\Eloquent\Attributes\Scope]
protected function someMethod()
protected function someMethod($query)
{

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class NonBuilderReturnType extends Model
{
public function scopeString(Builder $query): string
{
return 'foo';
}
}

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class NonBuilderReturnType extends Model
{
#[\Illuminate\Database\Eloquent\Attributes\Scope]
protected function string(Builder $query): string
{
return 'foo';
}
}

?>
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Model;
class NonDuplicateAttributeNodes extends Model
{
#[\Illuminate\Database\Eloquent\Attributes\Scope]
protected function scopeSomeMethod()
protected function scopeSomeMethod($query)
{

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model;

class NonDuplicateMethod extends Model
{
public function scopeSomeMethod()
public function scopeSomeMethod($query)
{

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class ScopeWithExtraParameters extends Model
{
public function scopeOfType(Builder $query, string $type)
{
return $query->where('type', $type);
}
}

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class ScopeWithExtraParameters extends Model
{
#[\Illuminate\Database\Eloquent\Attributes\Scope]
protected function ofType(Builder $query, string $type)
{
return $query->where('type', $type);
}
}

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

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Model;

class SkipNonBuilderFirstParam extends Model
{
public function scopeCategory(string $category): void
{
}
}

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

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

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

class SkipRelationMethods extends Model
{
public function scopeTypes(): HasMany
{
return $this->hasMany(ScopeType::class);
}
}

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

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class TypedBuilderParameter extends Model
{
public function scopeActive(Builder $query)
{
return $query->where('active', 1);
}
}

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\ScopeNamedClassMethodToScopeAttributedClassMethodRector\Fixture;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class TypedBuilderParameter extends Model
{
#[\Illuminate\Database\Eloquent\Attributes\Scope]
protected function active(Builder $query)
{
return $query->where('active', 1);
}
}

?>