How to write a Rector to fix Laravel 10 database expressions

remarkablemark
2 min readDec 21, 2023
Blue and white elephant with “php” letters on a laptop with code on the screen
Photo by Ben Griffiths on Unsplash

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

--

--