From 365af73635cce99103c9f0ab9b44ae97759f05d7 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 29 May 2020 13:54:45 -0400 Subject: [PATCH] Add support for custom PDO statement class WireDatabasePDOStatement. This is used rather than PDOStatement when in debug mode, so that it can translate bind values to actual values in queries that are used in the debug mode query log. --- wire/core/WireDatabasePDO.php | 60 +++++---- wire/core/WireDatabasePDOStatement.php | 174 +++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 28 deletions(-) create mode 100644 wire/core/WireDatabasePDOStatement.php diff --git a/wire/core/WireDatabasePDO.php b/wire/core/WireDatabasePDO.php index f4146e7c..c614b9ba 100644 --- a/wire/core/WireDatabasePDO.php +++ b/wire/core/WireDatabasePDO.php @@ -52,6 +52,8 @@ class WireDatabasePDO extends Wire implements WireDatabase { /** * Instance of PDO * + * @var \PDO + * */ protected $pdo = null; @@ -178,6 +180,14 @@ class WireDatabasePDO extends Wire implements WireDatabase { $config = $this->wire('config'); $this->stripMB4 = $config->dbStripMB4 && strtolower($config->dbEngine) != 'utf8mb4'; $this->queryLogMax = (int) $config->dbQueryLogMax; + if($config->debug && $this->pdo) { + // custom PDO statement for debug mode + $this->debugMode = true; + $this->pdo->setAttribute( + \PDO::ATTR_STATEMENT_CLASS, + array(__NAMESPACE__ . "\\WireDatabasePDOStatement", array($this)) + ); + } $sqlModes = $config->dbSqlModes; if(is_array($sqlModes)) { // ["5.7.0" => "remove:mode1,mode2/add:mode3"] @@ -217,8 +227,6 @@ class WireDatabasePDO extends Wire implements WireDatabase { $this->pdoConfig['pass'], $this->pdoConfig['options'] ); - // custom PDO statement for later maybe - // $this->pdo->setAttribute(\PDO::ATTR_STATEMENT_CLASS,array(__NAMESPACE__.'\WireDatabasePDOStatement',array($this))); } if(!$this->init) $this->_init(); return $this->pdo; @@ -437,8 +445,16 @@ class WireDatabasePDO extends Wire implements WireDatabase { $note = $driver_options; $driver_options = array(); } - if($this->debugMode) $this->queryLog($statement, $note); - return $this->pdo()->prepare($statement, $driver_options); + $pdoStatement = $this->pdo()->prepare($statement, $driver_options); + if($this->debugMode) { + if($pdoStatement instanceof WireDatabasePDOStatement) { + /** @var WireDatabasePDOStatement $pdoStatement */ + $pdoStatement->setDebugNote($note); + } else { + $this->queryLog($statement, $note); + } + } + return $pdoStatement; } /** @@ -567,26 +583,25 @@ class WireDatabasePDO extends Wire implements WireDatabase { * * @param string $sql Query (string) to log * @param string $note Any additional debugging notes about the query - * @return array|bool Returns query log array, or boolean true if you've logged a query + * @return array|bool|int Returns query log array, boolean true if added, boolean false if not * */ public function queryLog($sql = '', $note = '') { if(empty($sql)) return $this->queryLog; - if($this->debugMode) { - if(count($this->queryLog) > $this->queryLogMax) { - if(isset($this->queryLog['error'])) { - $qty = (int) $this->queryLog['error']; - } else { - $qty = 0; - } - $qty++; - $this->queryLog['error'] = "$qty additional queries omitted because \$config->dbQueryLogMax = $this->queryLogMax"; + if(!$this->debugMode) return false; + if(count($this->queryLog) > $this->queryLogMax) { + if(isset($this->queryLog['error'])) { + $qty = (int) $this->queryLog['error']; } else { - $this->queryLog[] = $sql . ($note ? " -- $note" : ""); - return true; + $qty = 0; } + $qty++; + $this->queryLog['error'] = "$qty additional queries omitted because \$config->dbQueryLogMax = $this->queryLogMax"; + return false; + } else { + $this->queryLog[] = $sql . ($note ? " -- $note" : ""); + return true; } - return false; } /** @@ -1002,14 +1017,3 @@ class WireDatabasePDO extends Wire implements WireDatabase { } } -/** - * custom PDOStatement for later maybe - * -class WireDatabasePDOStatement extends \PDOStatement { - protected $database; - protected function __construct(WireDatabasePDO $database) { - $this->database = $database; - // $database->message($this->queryString); - } -} - */ diff --git a/wire/core/WireDatabasePDOStatement.php b/wire/core/WireDatabasePDOStatement.php new file mode 100644 index 00000000..6173c856 --- /dev/null +++ b/wire/core/WireDatabasePDOStatement.php @@ -0,0 +1,174 @@ + "param value" ] + * + * @var array + * + */ + protected $debugParams = array(); + + /** + * Debug params that require PCRE, in format [ "/:param_name\b/" => "param value" ] + * + * @var array + * + */ + protected $debugParamsPCRE = array(); + + /** + * Quantity of debug params + * + * @var int + * + */ + protected $debugParamsQty = 0; + + /** + * Debug note + * + * @var string + * + */ + protected $debugNote = ''; + + /** + * Construct + * + * PDO requires the PDOStatement constructor to be protected for some reason + * + * @param WireDatabasePDO $database + * + */ + protected function __construct(WireDatabasePDO $database) { + $this->database = $database; + } + + /** + * Set debug note + * + * @param string $note + * + */ + public function setDebugNote($note) { + $this->debugNote = $note; + } + + /** + * Set a named debug parameter + * + * @param string $parameter + * @param int|string|null $value + * @param int|null $data_type \PDO::PARAM_* type + * + */ + public function setDebugParam($parameter, $value, $data_type = null) { + if($data_type === \PDO::PARAM_INT) { + $value = (int) $value; + } else if($data_type === \PDO::PARAM_NULL) { + $value = 'NULL'; + } else { + $value = $this->database->quote($value); + } + if($parameter[strlen($parameter)-1] !== 'X') { + // user-specified param name: partial name collisions possible, so use boundary + $this->debugParamsPCRE['/' . $parameter . '\b/'] = $value; + } else { + // auto-generated param name: already protected against partial name collisions + $this->debugParams[$parameter] = $value; + } + $this->debugParamsQty++; + } + + /** + * Bind a value for this statement + * + * @param string|int $parameter + * @param mixed $value + * @param int $data_type + * @return bool + * + */ + public function bindValue($parameter, $value, $data_type = \PDO::PARAM_STR) { + $result = parent::bindValue($parameter, $value, $data_type); + if(strpos($parameter, ':') === 0) { + $this->setDebugParam($parameter, $value, $data_type); + } else { + // note we do not handle index/question-mark parameters for debugging + } + return $result; + } + + /** + * Execute prepared statement + * + * @param array|null $input_parameters + * @return bool + * + */ + public function execute($input_parameters = NULL) { + + $timer = Debug::startTimer(); + $result = parent::execute($input_parameters); + $timer = Debug::stopTimer($timer, 'ms'); + + if(!$this->database) return $result; + + if(is_array($input_parameters)) { + foreach($input_parameters as $key => $value) { + $this->setDebugParam($key, $value); + } + } + + $debugNote = trim("$this->debugNote [$timer]"); + + if($this->debugParamsQty) { + $sql = $this->queryString; + if(count($this->debugParams)) { + $sql = str_replace( + array_keys($this->debugParams), + array_values($this->debugParams), + $sql + ); + } + if(count($this->debugParamsPCRE)) { + $sql = preg_replace( + array_keys($this->debugParamsPCRE), + array_values($this->debugParamsPCRE), + $sql + ); + } + $this->database->queryLog($sql, $debugNote); + } else { + $this->database->queryLog($this->queryString, $debugNote); + } + + return $result; + } + +}