問題
Laravel におけるデータベース操作でクエリエラーが発生したとき, PDOException
はキャッチされて QueryException
に変換される。これは生成時に
- 元のプレースホルダ
?
入りのSQL文 - バインドパラメータの配列
を受け取り,メッセージに ?
を実際の値に置換したSQLをセットしてくれる機能を持っている。しかし,整数値以外は正常に表示されない。クオート処理が行われていないためである。
\DB::statement('INSERT INTO invalid_grammar(?, ?, ?)', [1, null, 'xxx']);
Illuminate/Database/QueryException with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1, NULL, 'xxx')' at line 1 (SQL: INSERT INTO invalid_grammar(1, , xxx))'
INSERT INTO invalid_grammar(1, , xxx)
↓
INSERT INTO invalid_grammar(1, null, 'xxx')
こうなってほしい!
解決策
いつものようにコネクションクラスの継承をやっちゃいます。
【Laravel】 MySQL がマスタスレーブ構成のとき,リクエストを超えて sticky 効果を適用する - Qiita
この辺でもすでにやってるけど一応今回も載せておきます。
実装
<?php
declare(strict_types=1);
namespace App\Database;
use Illuminate\Database\MySqlConnection as BaseMySqlConnection;
/**
* Class MySqlConnection
*/
class MySqlConnection extends BaseMySqlConnection
{
/**
* Run a SQL statement.
*
* @param string $query
* @param array $bindings
* @param \Closure $callback
* @throws \App\Database\QueryException
* @return mixed
*/
protected function runQueryCallback($query, $bindings, Closure $callback)
{
try {
$result = $callback($query, $bindings);
} catch (\Exception $e) {
throw new QueryException(
$query, $this->prepareBindings($bindings), $e, $this->getPdo()
);
}
return $result;
}
}
<?php
declare(strict_types=1);
namespace App\Providers;
use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
public function boot(): void
{
Connection::resolverFor('mysql', function (...$parameters) {
return new MySqlConnection(...$parameters);
});
}
}
<?php
declare(strict_types=1);
namespace App\Database;
use Illuminate\Database\QueryException as BaseQueryException;
use Illuminate\Support\Str;
class QueryException extends BaseQueryException
{
/**
* @var \PDO
*/
protected $pdo;
/**
* QueryException constructor.
*
* @param string $sql
* @param array $bindings
* @param \Exception $previous
* @param \PDO $pdo
*/
public function __construct(string $sql, array $bindings, \Exception $previous, \PDO $pdo)
{
$this->pdo = $pdo;
parent::__construct($sql, $bindings, $previous);
}
/**
* Format the SQL error message.
*
* @param string $sql
* @param array $bindings
* @param \Exception $previous
* @return string
*/
protected function formatMessage($sql, $bindings, $previous)
{
return $previous->getMessage() . ' (SQL: ' . Str::replaceArray('?', $this->quoteBindings($bindings), $sql) . ')';
}
/**
* Quote binding parameters.
*
* @param array $bindings
* @return array
*/
protected function quoteBindings(array $bindings): array
{
return collect($bindings)->map(function ($value) {
if ($value === null) {
return 'null';
}
if (is_string($value)) {
return $this->pdo->quote($value);
}
if (is_bool($value)) {
return (int)$value;
}
return $value;
})->toArray();
}
}
実行結果
\DB::statement('INSERT INTO invalid_grammar(?, ?, ?)', [1, null, 'xxx']);
App/Database/QueryException with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1, NULL, 'xxx')' at line 1 (SQL: INSERT INTO invalid_grammar(1, null, 'xxx'))'
これで Sequel Pro とかにコピペして検証もやりやすくなるかも!