0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PDOとPDOStatementを掩蔽し、実行したSQLのログを出力する方法

Last updated at Posted at 2022-06-09
../

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にパラメータを差し込む部分も掩蔽してみた。

UserDataAccessor.php
<?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にハードコーディングしておく。

CommonProps.php
<?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を発行する場合に対応できないので注意のこと。実装はできているのだが、若干複雑になるので提示を割愛している。)

PersistenceBase.php
<?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());
    }
  }
}
../
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?