../ |
---|
PHPでDBにアクセスするためのベースクラスを作成してみた。PHPでは、ブラウザで画面が遷移するごと(redirectするごと)に新たなプロセスが生成されるので、DBへの接続情報をプーリングしたり、セッション($_SESSION)に保持したり、メモリ上にキャッシュしても意味がないことが分かってきた。新たなプロセスで必要となったときに、DBに接続し、PDOを生成する。そのプロセスが有効である間は、PDOを使い回して複数のSQLを発行できる。
SQLの発行は、プレペアステートメントを利用したいが、SQLのログが綺麗に出力できない。調べてみてもいい方法が見つからなかったので、独自に書いてみた。データ型によっては動作は未確認だが、int,bool,float,string,dateなどの基本型については問題ないので、紹介しておきたい。
SQLをログ出力できるプレペアステートメントの実現
例えば、DBのユーザー情報にアクセスするためのクラス(DAO)をUserDataAccessorとして、ユーザー取得のメソッドgetUser()とパスワード変更のメソッドupdatePassword()を以下のように記述したいと想定する。プレペアステートメントを利用していることが分かるだろう。しかし、PDOは掩蔽され、更新系ではPDOStatementも掩蔽されている。参照系は、fetchAll()を使う場合にはPDOStatementを掩蔽できるが、多くの場合にはfetch()を使いたいのでPDOStatementは見えるようにしている。query()、execute()のメソッドでは、「?」を含んだSQLにパラメータを差し込む部分も掩蔽してみた。
<?php
namespace users\integration;
require_once(__DIR__."/../../common/integration/PersistenceBase.php");
require_once(__DIR__."/../domain/User.php");
use common\integration\PersistenceBase;
use users\domain\User;
class UserDataAccessor extends PersistenceBase {
public function getUser(string $uid) {
$user = null;
$sql = "select * from user where user_id=?";
$stmt = $this->query($sql, $uid);
if ($stmt != null){
if ($row = $stmt->fetch()) {
$user = $this->createUser($row);
}
$this->logQuery($stmt);
}
return $user;
}
public function updatePassword(User &$user, string $pwd): int {
$sql = "update user set password=? where user_id=?";
$p1 = $pwd;
$p2 = $user->getUid();
$cnt = $this->execute($sql, $p1, $p2);
if ($cnt > 0) {
$user->setPassword($pwd);
}
return $cnt;
}
}
これらの2つのメソッドを以下のように実行してみる。CommonProps::$propsは、DBの接続先の情報を保持したarrayである。後述する。
$accessor = new UserDataAccessor(CommonProps::$props);
$user = $accessor->getUser('U001');
$accessor->updatePassword($user, 'q5Nj6atX');
実行されたSQLと処理した件数を、以下の感じでログ出力したい。表示されるSQLは「?」にパラメータが差し込まれていることが目標である。ログは、参照系ではlogQuery()で、更新系ではexecute()内で出力する仕様にしてみた。
[16:04:xx 2022] @@@ select * from user where user_id='U001' --> 1
[16:06:xx 2022] @@@ update user set password='q5Nj6atX' where user_id='U001' --> 1
ログのSQL部分をコピペしてMySQLコンソールに貼り付けたら、そのまま実行できるレベルにしておきたい。何件を検索したのか、何件を処理したのかをログ出力したい。DBの接続先情報は、CommonProps::propsというarrayにハードコーディングしておく。
<?php
namespace common\integration;
class CommonProps {
public static $props = [
'db.dbms' => 'mysql',
'db.dbname' => 'test',
'db.host' => 'localhost',
'db.port' => '3306',
'db.charset' => 'utf8',
'db.user' => 'root',
'db.password' => 'admin',
];
public static function __initialize(){
$a = &self::$props;
$a['db.dsn'] = $a['db.dbms']
. ":dbname=". $a['db.dbname']
. ";host=" . $a['db.host']
. ";port=" . $a['db.port']
. ";charset=" . $a['db.charset'];
}
}
CommonProps::__initialize();
PDOやPDOStatementを掩蔽するためのベースクラスの実装
UserDataAccessorが継承するPersistenceBaseは、以下のように実装してみた。PDOやPDOStatementを掩蔽し、発行したSQLと処理した件数をログ出力できるようにしている。基本的なデータ型は、内部でbindValue()できるようにしている。(※提示のコードでは、query()中に新たにSQLを発行する場合に対応できないので注意のこと。実装はできているのだが、若干複雑になるので提示を割愛している。)
<?php
namespace common\integration;
use PDO;
use PDOException;
use PDOStatement;
class PersistenceBase {
private $props;
private $pdo;
private $isTrace;
private $lastlog;
public function __construct(array &$props) {
$this->props = $props;
$this->isTrace = true;
}
private function pdo(): PDO {
if (empty($this->pdo)){
$dsn = $this->props['db.dsn'];
$user = $this->props['db.user'];
$password = $this->props['db.password'];
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
// PDO::ATTR_EMULATE_PREPARES, false,
PDO::ATTR_PERSISTENT => true
];
try {
$this->pdo = new PDO($dsn, $user, $password, $options);
error_log("@@@ connected " . $dsn);
} catch (PDOException $e) {
error_log('@@@ connect failed: ' . $e->getMessage());
}
}
return $this->pdo;
}
public function execute($sql, ...$param): int {
$log = &$sql;
$cnt = 0;
try {
$stmt = $this->pdo()->prepare($sql);
$this->appendParam($stmt, $log, $param);
$stmt->execute();
$cnt = $stmt->rowCount();
$this->logCount($log, $cnt);
} catch (PDOException $e) {
$this->logError($log, $e);
}
return $cnt;
}
public function fetchAll($sql, ...$param) {
$log = &$sql;
$rows = null;
try {
$stmt = $this->pdo()->prepare($sql);
$this->appendParam($stmt, $log, $param);
$stmt->execute();
$rows = $stmt->fetchAll();
$this->logCount($log, $stmt->rowCount());
} catch (PDOException $e) {
$this->logError($log, $e);
}
return $rows;
}
public function query($sql, ...$param) {
$log = &$sql;
$stmt = null;
try {
$stmt = $this->pdo()->prepare($sql);
$this->appendParam($stmt, $log, $param);
$stmt->execute();
$this->lastlog = $log;
} catch (PDOException $e) {
$this->logError($log, $e);
$stmt = null;
}
return $stmt;
}
public function logQuery(PDOStatement &$stmt): void {
if ($this->isTrace){
$this->logCount($this->lastlog, $stmt->rowCount());
}
$stmt = null;
}
private function appendParam(&$stmt, &$log, $param): void {
$n = 1;
foreach ($param as $p){
if (is_null($p)){
$stmt->bindValue($n, null, PDO::PARAM_NULL);
$this->appendValue($log, null);
} else if (is_bool($p)){
$stmt->bindValue($n, $p, PDO::PARAM_BOOL);
$this->appendValue($log, $p);
} else if (is_int($p)){
$stmt->bindValue($n, $p, PDO::PARAM_INT);
$this->appendValue($log, $p);
} else {
$stmt->bindValue($n, $p, PDO::PARAM_STR);
$this->appendValue($log, $p, true);
}
$n++;
}
}
private function appendValue(string &$log, $p, bool $quote = false): void {
if ($this->isTrace){
$n = strpos($log, '?');
if ($quote){
$p = '\'' . $p . '\'';
}
$log = substr($log, 0, $n) . $p . substr($log, $n + 1);
}
}
private function logCount(string &$sql, int $cnt): void {
if ($this->isTrace){
error_log('@@@ ' . $sql . ' --> ' . $cnt);
}
}
private function logError(string &$sql, PDOException &$e): void {
if ($this->isTrace){
error_log('@@@ ' . $sql . ' --> ' . $e->getMessage());
} else {
error_log('@@@ ' . $e->getMessage());
}
}
}
../ |
---|