🙃

FW使えなくて不便なのでPDOラッパー書いた。

2021/08/19に公開

初めに

現場でFW等使えず毎度PDOをnewするのがだるかったので必要最低限の機能を持ったPDOラッパーを書きました。今後おそらく使うことはないと思いますが念のための自分へのメモとして記事にします。
https://github.com/notpop/pdo_wrapper

現場で書いたのを思い出しながら自宅で書いただけなのでもしかしたら誤字とかあるかも....。
エラー出ちゃったら教えて下さい、お願い致します。一応エディタ上でエラーがない程度は確認しています。

ソース

class Database
{
  const LOG_FILE_PATH = '';

  private $pdo;
  private $query;
  private $binds = [];
  private $fetch_pattern = PDO::FETCH_ASSOC;

  function __construct($database, $user, $password) {
    $pdo = new PDO($database, $user, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    $this->pdo = $pdo;
  }

  private static function isNullOrEmpty($string) {
    if (is_null($string) || '' == $string) {
      return true;
    }

    return false;
  }

  public function destroy() {
    $this->pdo = null;
  }

  private function commit() {
    $this->pdo->commit();
  }

  private function rollback() {
    $this->pdo->rollback();
  }

  public function setQuery($query) {
    if ( ! self::isNullOrEmpty($query)) {
      $old_query = $this->query;
      $this->query = $old_query . $query;
    }
  }

  private function resetQuery() {
    $this->query = null;
  }

  public function setBind($key, $value, $parameter = null) {
    $this->binds[$key] = [
      'value'     => $value,
      'parameter' => $parameter
    ];
  }

  public function setBindInt($key, $value) {
    $this->setBind($key, $value, PDO::PARAM_INT);
  }

  public function setBindString($key, $value) {
    $this->setBind($key, $value, PDO::PARAM_STR);
  }

  private function resetBind() {
    $this->binds = [];
  }

  public function setFetchPattern($pattern) {
    if ( ! self::isNullOrEmpty($pattern)) {
      $this->fetch_pattern = $pattern;
    }
  }

  private function resetFetchPattern() {
    $this->fetch_pattern = PDO::FETCH_ASSOC;
  }

  private function resetInstance() {
    $this->resetQuery();
    $this->resetBind();
    $this->resetFetchPattern();
  }

  public function select() {
    $pdo = $this->pdo;
    if (self::isNullOrEmpty($pdo)) {
      throw new Exception('PDO instance is not found.', '9999');
    }

    $query = $this->query;
    $binds = $this->binds;
    $fetch_pattern = $this->fetch_pattern;

    try {
      $statement = $pdo->prepare($query);

      foreach ($binds as $key => $bind) {
        $value = $bind['value'];
        $paramter = $bind['parameter'];

        if (self::isNullOrEmpty($parameter)) {
          $statement->bindValue($key, $value);
        }
        else {
          $statement->bindValue($key, $value, $parameter);
        }
      }

      $statement->execute();
      $result = $statement->fetchAll($fetch_pattern);

      $pdo = null;

      $this->resetInstance();

      return $result;
    }
    catch (Exception $exceptions) {
      $error_line = $exceptions->getLine();
      $error_code = $exceptions->getCode();
      $error_message = $exceptions->getMessage();

      $error_log = date('Y-m-d H:i:s') . ' LINE:' . $error_line . ' CODE:' . $error_code . ' MESSAGE:' . $error_message . '¥n';
      error_log($error_log, 3, LOG_FILE_PATH);

      $pdo = null;
      $this->destroy();
      $this->resetInstance();

      return null;
    }
  }

  public function selectOne() {
    $pdo = $this->pdo;
    if (self::isNullOrEmpty($pdo)) {
      throw new Exception('PDO instance is not found.', '9999');
    }

    $query = $this->query;
    $binds = $this->binds;

    try {
      $statement = $pdo->prepare($query);

      foreach ($binds as $key => $bind) {
        $value = $bind['value'];
        $paramter = $bind['parameter'];

        if (self::isNullOrEmpty($parameter)) {
          $statement->bindValue($key, $value);
        }
        else {
          $statement->bindValue($key, $value, $parameter);
        }
      }

      $statement->execute();
      $result = $statement->fetchColumn();

      $pdo = null;

      $this->resetInstance();

      return $result;
    }
    catch (Exception $exceptions) {
      $error_line = $exceptions->getLine();
      $error_code = $exceptions->getCode();
      $error_message = $exceptions->getMessage();

      $error_log = date('Y-m-d H:i:s') . ' LINE:' . $error_line . ' CODE:' . $error_code . ' MESSAGE:' . $error_message . '¥n';
      error_log($error_log, 3, LOG_FILE_PATH);

      $pdo = null;
      $this->destroy();
      $this->resetInstance();

      return null;
    }
  }

  public function update() {
    $pdo = $this->pdo;
    if (self::isNullOrEmpty($pdo)) {
      throw new Exception('PDO instance is not found.', '9999');
    }

    $query = $this->query;
    $binds = $this->binds;

    try {
      $pdo->beginTransaction();

      $statement = $pdo->prepare($query);

      foreach ($binds as $key => $bind) {
        $value = $bind['value'];
        $paramter = $bind['parameter'];

        if (self::isNullOrEmpty($parameter)) {
          $statement->bindValue($key, $value);
        }
        else {
          $statement->bindValue($key, $value, $parameter);
        }
      }

      $statement->execute();
      if (0 == $statement->rowCount()) {
        throw new Exception('update is failed.', '9997');
      }

      $this->commit();

      $pdo = null;

      $this->resetInstance();

      return true;
    }
    catch (Exception $exceptions) {
      $this->rollback();

      $error_line = $exceptions->getLine();
      $error_code = $exceptions->getCode();
      $error_message = $exceptions->getMessage();

      $error_log = date('Y-m-d H:i:s') . ' LINE:' . $error_line . ' CODE:' . $error_code . ' MESSAGE:' . $error_message . '¥n';
      error_log($error_log, 3, LOG_FILE_PATH);

      $pdo = null;
      $this->destroy();
      $this->resetInstance();

      return false;
    }
  }

  public function insert() {
    $pdo = $this->pdo;
    if (self::isNullOrEmpty($pdo)) {
      throw new Exception('PDO instance is not found.', '9999');
    }

    $query = $this->query;
    $binds = $this->binds;

    try {
      $pdo->beginTransaction();

      $statement = $pdo->prepare($query);

      foreach ($binds as $key => $bind) {
        $value = $bind['value'];
        $paramter = $bind['parameter'];

        if (self::isNullOrEmpty($parameter)) {
          $statement->bindValue($key, $value);
        }
        else {
          $statement->bindValue($key, $value, $parameter);
        }
      }

      $statement->execute();
      if (0 == $statement->rowCount()) {
        throw new Exception('insert is failed.', '9998');
      }

      $this->commit();

      $pdo = null;

      $this->resetInstance();

      return true;
    }
    catch (Exception $exceptions) {
      $this->rollback();

      $error_line = $exceptions->getLine();
      $error_code = $exceptions->getCode();
      $error_message = $exceptions->getMessage();

      $error_log = date('Y-m-d H:i:s') . ' LINE:' . $error_line . ' CODE:' . $error_code . ' MESSAGE:' . $error_message . '¥n';
      error_log($error_log, 3, LOG_FILE_PATH);

      $pdo = null;
      $this->destroy();
      $this->resetInstance();

      return false;
    }
  }
}

思考

ソースを読んでいただければ違和感を持つ方もいらっしゃるかもしれませんが私自身も違和感を持ったのでその感覚は正常かと思います笑

具体的な場所ですがupdate()とinsert()の

$this->commit();
$this->rollback();

ではないでしょうか?

先に結論を申し上げるとこいつらは意図した通り正常に動作しますのでご安心くださいませ。

初め自分でささっと書いた時は全く何も考えてなかったので「こんな感じで行けるやろ!!」程度に思ってまして、最終確認で見返している際に気になって調べることにしました。

まず散々画面で登録、更新をやっていて一切おかしな動作をしたことはなかったけれど、念のために正常に動作するかの確認を行うためにthis->commit()の前後にthrowを仕込んでそれぞれ動かして見た所、前の時は更新されず後の時は「transactionおらんで!」って怒られました。(There is no active transaction.)commit後にrollbackしようとしてるからそりゃ当然なんです。
まあ、問題なさそうかなと。

変数pdoにthis->pdoを代入して別物になってるのに何でthis->commit()とthis->rollback()は正常に動作するんやろ???とこの時点では考えておりそれ前提で色々思考を重ねました。
(実はこの前提がそもそも違う)

上記の事が成り立ち得るパターンは単にPDOはDBに対してコマンドを流しに行っているだけで同じ接続情報を持ってるからなりたっているのでは?
ただこの考え方だと同じタイミングで他からのコマンド実行あった時どうすんの?みたいなことも考えてました。

それでまたPHPの初歩的な知識の抜けが発覚しました泣

「PHPのオブジェクトは参照型なので明示的に指定しない限り参照される」
つまり別物だと思っていた変数pdoとメンバ変数this->pdoは同じだったと言う話でした。

なんか最近こう言うの多くね?・・・

とは言え直感的にも分かりずらいので気になる方は直接

$pdo->commit();
$pdo->rollback();

に書き換えてください。

今回は経緯を残したいと言う意図もあるので自分はこのままにしておきます。

他にも気になる所がある方はコメントください。

では今回はここらで、ありがとうございました!!

追記 2021/8/21

これgithubには書いてるんですがModel、つまりこの記事のDatabaseを何やかんや使ってるclassを当然作ってそいつをControllerから呼び出すんですが処理終わりにdestroy()って関数を用意して一々呼ぶようにしています。

その上で__destruct()使えばそんなことせんでええやんということに偶々ドキュメント読んで気づきました。
明示的に可読性を上げるためにあえて記載するのは良いと思いますが何もないなら__destruct()活用したほうが良さそうです。

Discussion