diff --git a/src/ORM/Connect/PDOConnector.php b/src/ORM/Connect/PDOConnector.php index 880621423..c26cc02bb 100644 --- a/src/ORM/Connect/PDOConnector.php +++ b/src/ORM/Connect/PDOConnector.php @@ -98,7 +98,7 @@ class PDOConnector extends DBConnector implements TransactionManager * one exists for the given query * * @param string $sql - * @return PDOStatement + * @return PDOStatementHandle|false */ public function getOrPrepareStatement($sql) { @@ -113,11 +113,14 @@ class PDOConnector extends DBConnector implements TransactionManager array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY) ); + // Wrap in a PDOStatementHandle, to cache column metadata + $statementHandle = ($statement === false) ? false : new PDOStatementHandle($statement); + // Only cache select statements if (preg_match('/^(\s*)select\b/i', $sql)) { - $this->cachedStatements[$sql] = $statement; + $this->cachedStatements[$sql] = $statementHandle; } - return $statement; + return $statementHandle; } /** @@ -214,7 +217,10 @@ class PDOConnector extends DBConnector implements TransactionManager $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca']; } // use default cipher if not provided - $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default'); + $options[PDO::MYSQL_ATTR_SSL_CIPHER] = + array_key_exists('ssl_cipher', $parameters) ? + $parameters['ssl_cipher'] : + self::config()->get('ssl_cipher_default'); } if (static::config()->get('legacy_types')) { @@ -328,7 +334,11 @@ class PDOConnector extends DBConnector implements TransactionManager $statement = $this->pdoConnection->query($sql); // Generate results - return $this->prepareResults($statement, $errorLevel, $sql); + if ($statement === false) { + $this->databaseError($this->getLastError(), $errorLevel, $sql); + } else { + return $this->prepareResults(new PDOStatementHandle($statement), $errorLevel, $sql); + } } /** @@ -395,52 +405,49 @@ class PDOConnector extends DBConnector implements TransactionManager { $this->beforeQuery($sql); - // Prepare statement - $statement = $this->getOrPrepareStatement($sql); + // Fetch cached statement, or create it + $statementHandle = $this->getOrPrepareStatement($sql); - // Bind and invoke statement safely - if ($statement) { - $this->bindParameters($statement, $parameters); - $statement->execute($parameters); + // Error handling + if ($statementHandle === false) { + $this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters)); + return null; } + // Bind parameters + $this->bindParameters($statementHandle->getPDOStatement(), $parameters); + $statementHandle->execute($parameters); + // Generate results - return $this->prepareResults($statement, $errorLevel, $sql); + return $this->prepareResults($statementHandle, $errorLevel, $sql); } /** * Given a PDOStatement that has just been executed, generate results * and report any errors * - * @param PDOStatement $statement + * @param PDOStatementHandle $statement * @param int $errorLevel * @param string $sql * @param array $parameters * @return PDOQuery */ - protected function prepareResults($statement, $errorLevel, $sql, $parameters = array()) + protected function prepareResults(PDOStatementHandle $statement, $errorLevel, $sql, $parameters = array()) { - // Record row-count and errors of last statement + // Catch error if ($this->hasError($statement)) { $this->lastStatementError = $statement->errorInfo(); - } elseif ($statement) { - // Count and return results - $this->rowCount = $statement->rowCount(); - return new PDOQuery($statement); - } - - // Ensure statement is closed - if ($statement) { $statement->closeCursor(); + + $this->databaseError($this->getLastError(), $errorLevel, $sql, $this->parameterValues($parameters)); + + return null; } - // Report any errors - if ($parameters) { - $parameters = $this->parameterValues($parameters); - } - $this->databaseError($this->getLastError(), $errorLevel, $sql, $parameters); - return null; + // Count and return results + $this->rowCount = $statement->rowCount(); + return new PDOQuery($statement); } /** diff --git a/src/ORM/Connect/PDOQuery.php b/src/ORM/Connect/PDOQuery.php index 61232bcdd..718fd6f8b 100644 --- a/src/ORM/Connect/PDOQuery.php +++ b/src/ORM/Connect/PDOQuery.php @@ -2,82 +2,30 @@ namespace SilverStripe\ORM\Connect; -use PDOStatement; -use PDO; - /** * A result-set from a PDO database. */ class PDOQuery extends Query { /** - * The internal MySQL handle that points to the result set. - * @var PDOStatement + * @var array */ - protected $statement = null; - protected $results = null; - protected static $type_mapping = [ - // PGSQL - 'float8' => 'float', - 'float16' => 'float', - 'numeric' => 'float', - - // MySQL - 'NEWDECIMAL' => 'float', - - // SQlite - 'integer' => 'int', - 'double' => 'float', - ]; - /** * Hook the result-set given into a Query class, suitable for use by SilverStripe. * @param PDOStatement $statement The internal PDOStatement containing the results */ - public function __construct(PDOStatement $statement) + public function __construct(PDOStatementHandle $statement) { - $this->statement = $statement; // Since no more than one PDOStatement for any one connection can be safely // traversed, each statement simply requests all rows at once for safety. // This could be re-engineered to call fetchAll on an as-needed basis - $this->results = $this->typeCorrectedFetchAll($statement); - + $this->results = $statement->typeCorrectedFetchAll(); $statement->closeCursor(); } - /** - * Fetch a record form the statement with its type data corrected - * Returns data as an array of maps - * @return array - */ - protected function typeCorrectedFetchAll($statement) - { - $columnCount = $statement->columnCount(); - $columnMeta = []; - for ($i = 0; $i<$columnCount; $i++) { - $columnMeta[$i] = $statement->getColumnMeta($i); - } - - // Re-map fetched data using columnMeta - return array_map( - function ($rowArray) use ($columnMeta) { - $row = []; - foreach ($columnMeta as $i => $meta) { - // Coerce any column types that aren't correctly retrieved from the database - if (isset($meta['native_type']) && isset(self::$type_mapping[$meta['native_type']])) { - settype($rowArray[$i], self::$type_mapping[$meta['native_type']]); - } - $row[$meta['name']] = $rowArray[$i]; - } - return $row; - }, - $statement->fetchAll(PDO::FETCH_NUM) - ); - } - public function seek($row) { $this->rowNum = $row - 1; diff --git a/src/ORM/Connect/PDOStatementHandle.php b/src/ORM/Connect/PDOStatementHandle.php new file mode 100644 index 000000000..259e71091 --- /dev/null +++ b/src/ORM/Connect/PDOStatementHandle.php @@ -0,0 +1,154 @@ +statement = $statement; + } + + /** + * Mapping of PDO-reported "native types" to PHP types + */ + protected static $type_mapping = [ + // PGSQL + 'float8' => 'float', + 'float16' => 'float', + 'numeric' => 'float', + + // MySQL + 'NEWDECIMAL' => 'float', + + // SQlite + 'integer' => 'int', + 'double' => 'float', + ]; + + /** + * Fetch a record form the statement with its type data corrected + * Returns data as an array of maps + * @return array + */ + public function typeCorrectedFetchAll() + { + if ($this->columnMeta === null) { + $columnCount = $this->statement->columnCount(); + $this->columnMeta = []; + for ($i = 0; $i<$columnCount; $i++) { + $this->columnMeta[$i] = $this->statement->getColumnMeta($i); + } + } + + // Re-map fetched data using columnMeta + return array_map( + function ($rowArray) { + $row = []; + foreach ($this->columnMeta as $i => $meta) { + // Coerce any column types that aren't correctly retrieved from the database + if (isset($meta['native_type']) && isset(self::$type_mapping[$meta['native_type']])) { + settype($rowArray[$i], self::$type_mapping[$meta['native_type']]); + } + $row[$meta['name']] = $rowArray[$i]; + } + return $row; + }, + $this->statement->fetchAll(PDO::FETCH_NUM) + ); + } + + /** + * Closes the cursor, enabling the statement to be executed again (PDOStatement::closeCursor) + * + * @return bool Returns true on success + */ + public function closeCursor() + { + return $this->statement->closeCursor(); + } + + /** + * Fetch the SQLSTATE associated with the last operation on the statement handle + * (PDOStatement::errorCode) + * + * @return string + */ + public function errorCode() + { + return $this->statement->errorCode(); + } + + /** + * Fetch extended error information associated with the last operation on the statement handle + * (PDOStatement::errorInfo) + * + * @return array + */ + public function errorInfo() + { + return $this->statement->errorInfo(); + } + + /** + * Returns the number of rows affected by the last SQL statement (PDOStatement::rowCount) + * + * @return int + */ + public function rowCount() + { + return $this->statement->rowCount(); + } + + /** + * Executes a prepared statement (PDOStatement::execute) + * + * @param $parameters An array of values with as many elements as there are bound parameters in the SQL statement + * being executed + * @return bool Returns true on success + */ + public function execute(array $parameters) + { + return $this->statement->execute($parameters); + } + + /** + * Return the PDOStatement that this object provides a handle to + * + * @return PDOStatement + */ + public function getPDOStatement() + { + return $this->statement; + } +}