Skip to content

Instantly share code, notes, and snippets.

@Gummibeer
Created May 7, 2022 08:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gummibeer/8bce2aa120563858167218fc68e03b1d to your computer and use it in GitHub Desktop.
Save Gummibeer/8bce2aa120563858167218fc68e03b1d to your computer and use it in GitHub Desktop.
<?php
namespace App\PHPStan\Rules;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
class QueryBuilderWhereOperatorRule implements Rule
{
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param \PhpParser\Node\Expr\MethodCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return array
*/
public function processNode(Node $node, Scope $scope): array
{
$name = $this->nodeName($node);
if($name === null || !str_starts_with($name, 'where')){
return [];
}
$classNode = $node;
do {
$classNode = $classNode->var;
} while(property_exists($classNode, 'var') && !$classNode instanceof StaticCall);
if(!$classNode instanceof StaticCall) {
return [];
}
if (! $this->isCalledOnModel($classNode)) {
return [];
}
$staticName = $this->nodeName($classNode);
if($staticName === null) {
return [];
}
$class = $classNode->class;
if (!$class instanceof FullyQualified) {
return [];
}
$object = new ObjectType($class->toString());
$method = $object->getMethod($staticName, $scope);
$returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
if(!$returnType instanceof ObjectType) {
return [];
}
if(!$this->isQueryBuilder($returnType)) {
return [];
}
$index = array_search(
'operator',
array_map(
fn(DummyParameter $parameter) => $parameter->getName(),
ParametersAcceptorSelector::selectSingle($returnType->getMethod($name, $scope)->getVariants())->getParameters()
)
);
if($index === false) {
return [];
}
$operator = $node->args[$index]->value;
if(!$operator instanceof Node\Scalar\String_) {
return [
RuleErrorBuilder::message("Called '{$returnType->getClassName()}->{$name}()' with a non-string \$operator.")
->identifier('rules.queryBuilderWhereOperator')
->line($node->getLine())
->file($scope->getFile())
->build(),
];
}
if(!in_array($operator->value, DB::connection()->query()->operators)) {
return [
RuleErrorBuilder::message("Called '{$returnType->getClassName()}->{$name}()' without an allowed \$operator.")
->identifier('rules.queryBuilderWhereOperator')
->line($node->getLine())
->file($scope->getFile())
->build(),
];
}
return [];
}
protected function nodeName(Node $node): ?string
{
$name = $node?->name;
if (! $name instanceof Identifier) {
return null;
}
return $name->name;
}
protected function isCalledOnModel(StaticCall $call): bool
{
$class = $call->class;
if ($class instanceof FullyQualified) {
$type = new ObjectType($class->toString());
} else {
return false;
}
return (new ObjectType(Model::class))
->isSuperTypeOf($type)
->yes();
}
protected function isQueryBuilder(ObjectType $type): bool
{
return (new ObjectType(Builder::class))->isSuperTypeOf($type)->yes()
|| (new ObjectType(EloquentBuilder::class))->isSuperTypeOf($type)->yes();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment