How to write a Rector to fix Laravel 10 database expressions
This article goes over how to write a Rector rule to fix Laravel 10 database expressions.
Problem
If you’re getting the error after upgrading to Laravel 10:
PDO::prepare (): Argument #1 ($query) must be of type string, Illuminate\Database|Query\ Expression given
This may be caused by DB::raw
no longer being casted into string
. For example, this no longer works:
DB::select(DB::raw('select 1'));
The upgrade document suggests using a different approach to get the string value from a database expression. For example:
DB::select(DB::raw('select 1')->getValue(DB::getQueryGrammar()));
So what can we do? Rector to the rescue!
Solution
Rector is a code refactoring tool for PHP.
Given we initialize a custom Rector rule:
use Rector\Core\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
final class LaravelDatabaseExpressionsRector extends AbstractRector;
{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
// ...
);
}
public function getNodeTypes(): array
{
// ...
}
public function refactor(Node $node): ?Node
{
// ...
}
}
To refactor to the following:
-DB::raw('select 1');
+DB::raw('select 1')->getValue(DB::getQueryGrammar());
You want to get nodes of type StaticCall
:
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [\PhpParser\Node\Expr\StaticCall::class];
}
Then refactor the node so it chains a MethodCall
with the argument:
/**
* @param StaticCall $node
*/
public function refactor(Node $node): ?Node
{
$className = isset($node->class) ? $this->getName($node->class) : '';
$methodName = $this->getName($node->name);
// skip if not `DB::raw`
if (str_ends_with($className, 'DB') || $methodName !== 'raw') {
return null;
}
// DB::getQueryGrammar()
$arguments[] = new Arg(
new StaticCall(
new Name('DB'),
'getQueryGrammar'
)
);
// DB::raw(...)->getValue(DB::getQueryGrammar())
$node->value = new MethodCall(
$node,
new Identifier('getValue'),
$arguments
);
// DB::raw(...)->getValue(DB::getQueryGrammar())
return $node;
}
But this will refactor all DB::raw
nodes. Instead, you want to check and refactor only the child node of DB::select
:
/**
* @param StaticCall $node
*/
public function refactor(Node $node): ?Node
{
$className = $this->getName($childNode->class);
$methodName = $this->getName($childNode->name);
/** @var Node */
$childNode = $node->args[0]->value ?? null;
$childClassName = isset($childNode->class) ? $this->getName($childNode->class) : '';
$childMethodName = $this->getName($childNode->name);
if (
// skip if not `DB::select`
str_ends_with($className, 'DB') || $methodName !== 'select' ||
// skip if not `DB::raw`
str_ends_with($childClassName, 'DB') || $childMethodName !== 'raw'
) {
return null;
}
// DB::getQueryGrammar()
$arguments[] = new Arg(
new StaticCall(
new Name('DB'),
'getQueryGrammar'
)
);
// DB::raw(...)->getValue(DB::getQueryGrammar())
$node->args[0]->value = new MethodCall(
$childNode,
new Identifier('getValue'),
$arguments
);
// DB::select(DB::raw(...)->getValue(DB::getQueryGrammar()))
return $node;
}
Running the Rector rule, it will refactor accordingly:
-DB::select(DB::raw('select 1'));
+DB::select(DB::raw('select 1')->getValue(DB::getQueryGrammar()));
Rule
Check out the Rector rule if you want to install and apply to your codebase:
composer require --dev remarkablemark/rector-laravel-database-expressions