Created
April 28, 2025 14:43
-
-
Save canvural/fa40bae878b9104381f5f2fb063fa2b2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Tools\Rector\Rules; | |
use Illuminate\Database\Eloquent\Model; | |
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; | |
use Illuminate\Database\Eloquent\Relations\BelongsToMany; | |
use Illuminate\Database\Eloquent\Relations\Relation; | |
use Illuminate\Database\Query\Builder as QueryBuilder; | |
use PhpParser\Node; | |
use PhpParser\Node\Expr\MethodCall; | |
use PHPStan\Analyser\Scope; | |
use PHPStan\Reflection\ClassReflection; | |
use PHPStan\Reflection\ReflectionProvider; | |
use PHPStan\Type\ObjectType; | |
use PHPStan\Type\ThisType; | |
use PHPStan\Type\Type; | |
use PHPStan\Type\TypeCombinator; | |
use Rector\Rector\AbstractScopeAwareRector; | |
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | |
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | |
class DynamicWhereToQueryBuilderWhereRector extends AbstractScopeAwareRector | |
{ | |
public function __construct(private readonly ReflectionProvider $reflectionProvider) {} | |
public function getRuleDefinition(): RuleDefinition | |
{ | |
return new RuleDefinition('Change dynamic where to query builder where() calls', [ | |
new CodeSample('$model->whereUserId(1)', '$model->where("user_id", 1)'), | |
]); | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function getNodeTypes(): array | |
{ | |
return [ | |
Node\Expr\MethodCall::class, | |
]; | |
} | |
/** | |
* @inheritDoc | |
*/ | |
public function refactorWithScope(Node $node, Scope $scope) | |
{ | |
if (! $node instanceof Node\Expr\MethodCall) { | |
return null; | |
} | |
$methodName = $this->getName($node->name); | |
if (! str_starts_with($methodName, 'where')) { | |
return null; | |
} | |
if ($node->args === []) { | |
return null; | |
} | |
$calledOnType = $this->getCalledOnType($node, $scope); | |
if (! $calledOnType instanceof ObjectType) { | |
return null; | |
} | |
if ( | |
$calledOnType->isInstanceOf(Model::class)->no() && | |
$calledOnType->isInstanceOf(EloquentBuilder::class)->no() && | |
$calledOnType->isInstanceOf(QueryBuilder::class)->no() && | |
$calledOnType->isInstanceOf(Relation::class)->no() | |
) { | |
return null; | |
} | |
/** @var ClassReflection|null $calledOnReflection */ | |
$calledOnReflection = $calledOnType->getClassReflection(); | |
if ($calledOnReflection === null) { | |
return null; | |
} | |
if ($calledOnReflection->hasNativeMethod($methodName)) { | |
return null; | |
} | |
$model = $this->findModel($calledOnReflection); | |
if ($model === Model::class) { | |
return null; | |
} | |
$eloquentBuilder = EloquentBuilder::class; | |
if ( | |
$this->reflectionProvider->getClass(Model::class)->hasNativeMethod($methodName) || | |
$model && $this->reflectionProvider->getClass($model)->hasNativeMethod('scope' . ucfirst($methodName)) || | |
$this->reflectionProvider->getClass($eloquentBuilder)->hasNativeMethod($methodName) || | |
$this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodName) || | |
$this->reflectionProvider->getClass(BelongsToMany::class)->hasNativeMethod($methodName) | |
) { | |
return null; | |
} | |
$node->name = new Node\Identifier('where'); | |
$node->args = [ | |
new Node\Arg(new Node\Scalar\String_( | |
strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', substr($methodName, 5)))) | |
), | |
new Node\Arg($node->args[0]->value), | |
]; | |
return $node; | |
} | |
private function getCalledOnType(MethodCall $node, Scope $scope): Type | |
{ | |
$calledOnType = $scope->getType($node->var); | |
if ($calledOnType instanceof ThisType) { | |
$calledOnType = $calledOnType->getStaticObjectType(); | |
} | |
return TypeCombinator::removeNull($calledOnType); | |
} | |
private function findModel(ClassReflection $calledOnReflection): string|null | |
{ | |
if ($calledOnReflection->isSubclassOf(Model::class)) { | |
return $calledOnReflection->getName(); | |
} | |
if ( | |
$calledOnReflection->getName() === EloquentBuilder::class || | |
$calledOnReflection->isSubclassOf(EloquentBuilder::class) | |
) { | |
$modelType = $calledOnReflection->getActiveTemplateTypeMap()->getType('TModelClass'); | |
if (! $modelType instanceof ObjectType) { | |
return null; | |
} | |
return $modelType->getClassName(); | |
} | |
if ( | |
$calledOnReflection->getName() === Relation::class || | |
$calledOnReflection->isSubclassOf(Relation::class) | |
) { | |
$modelType = $calledOnReflection->getActiveTemplateTypeMap()->getType('TRelatedModel'); | |
if (! $modelType instanceof ObjectType) { | |
return null; | |
} | |
return $modelType->getClassName(); | |
} | |
return null; | |
} | |
} |
@canvural Might be awesome if you make a PR of this rule to the driftingly/rector-laravel repo π
Yeah it's in my mind. But few things needs to be cleaned up first. This was a very hacky way to do it quickly.Just need to find time π
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@canvural Might be awesome if you make a PR of this rule to the driftingly/rector-laravel repo π