From 0e5761dd08a712adcdd3fddf23b653852d53980e Mon Sep 17 00:00:00 2001 From: Plamen Popov Date: Mon, 15 Dec 2014 09:49:34 +0200 Subject: [PATCH 1/2] tests for #85 profiling prepare statement fix --- tests/unit/src/ProfilerTest.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/unit/src/ProfilerTest.php b/tests/unit/src/ProfilerTest.php index 9c13e447..293561f8 100644 --- a/tests/unit/src/ProfilerTest.php +++ b/tests/unit/src/ProfilerTest.php @@ -15,7 +15,7 @@ public function testGetPdo() $lazy_pdo = $this->pdo->getPdo(); $this->assertInstanceOf('PDO', $lazy_pdo); } - + public function testProfiling() { $this->pdo->setProfiler(new Profiler); @@ -35,7 +35,8 @@ public function testProfiling() $this->pdo->fetchAll("SELECT 3 FROM pdotest", array('zim' => 'gir')); $profiles = $this->pdo->getProfiler()->getProfiles(); - $this->assertEquals(3, count($profiles)); + // 1 x query(), 1 x exec(), and 2 x fetchAll() which is executed as prepare() and perform() + $this->assertEquals(4, count($profiles)); // get the profiles, remove stuff that's variable $actual = $this->pdo->getProfiler()->getProfiles(); @@ -56,6 +57,11 @@ public function testProfiling() 'bind_values' => array(), ), 2 => array( + 'function' => 'prepare', + 'statement' => 'SELECT 3 FROM pdotest', + 'bind_values' => array(), + ), + 3 => array( 'function' => 'perform', 'statement' => 'SELECT 3 FROM pdotest', 'bind_values' => array( @@ -79,7 +85,7 @@ public function testResetProfiles() // activate $this->pdo->getProfiler()->setActive(true); - $this->pdo->query("SELECT 1 FROM pdotest"); + $this->pdo->query("SELECT 1 FROM pdotest"); $profiles = $this->pdo->getProfiler()->getProfiles(); $this->assertEquals(1, count($profiles)); @@ -108,7 +114,8 @@ public function testResetProfiles() $this->pdo->fetchAll("SELECT 3 FROM pdotest", array('zim' => 'gir')); $profiles = $this->pdo->getProfiler()->getProfiles(); - $this->assertEquals(2, count($profiles)); + // fetchAll() is executed as prepare() and perform() + $this->assertEquals(3, count($profiles)); // get the profiles, remove stuff that's variable $actual = $this->pdo->getProfiler()->getProfiles(); @@ -117,13 +124,18 @@ public function testResetProfiles() unset($actual[$key]['trace']); } - $expect = array( + $expect = array( 0 => array( 'function' => 'exec', 'statement' => 'SELECT 2 FROM pdotest', 'bind_values' => array(), ), 1 => array( + 'function' => 'prepare', + 'statement' => 'SELECT 3 FROM pdotest', + 'bind_values' => array(), + ), + 2 => array( 'function' => 'perform', 'statement' => 'SELECT 3 FROM pdotest', 'bind_values' => array( From 7cf2af9d9db8c92055ef4e3cb5a30f5108e21118 Mon Sep 17 00:00:00 2001 From: Plamen Popov Date: Mon, 15 Dec 2014 10:34:38 +0200 Subject: [PATCH 2/2] forgot #85 fix --- src/ExtendedPdo.php | 229 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 217 insertions(+), 12 deletions(-) diff --git a/src/ExtendedPdo.php b/src/ExtendedPdo.php index 63d290f8..0e72532f 100644 --- a/src/ExtendedPdo.php +++ b/src/ExtendedPdo.php @@ -39,6 +39,7 @@ class ExtendedPdo extends PDO implements ExtendedPdoInterface */ protected $attributes = array( self::ATTR_ERRMODE => self::ERRMODE_EXCEPTION, + self::ATTR_EMULATE_PREPARES => true, ); /** @@ -95,15 +96,6 @@ class ExtendedPdo extends PDO implements ExtendedPdoInterface */ protected $username; - /** - * - * A specialized statement preparer. - * - * @var Rebuilder - * - */ - protected $rebuilder; - /** * * This constructor is pseudo-polymorphic. You may pass a normal set of PDO @@ -712,7 +704,10 @@ public function perform($statement, array $values = array()) public function prepare($statement, $options = array()) { $this->connect(); - return $this->pdo->prepare($statement, $options); + $this->beginProfile(__FUNCTION__); + $sth = $this->pdo->prepare($statement, $options); + $this->endProfile($statement, $options); + return $sth; } /** @@ -914,9 +909,13 @@ public function prepareWithValues($statement, array $values = array()) return $this->prepare($statement); } + // match standard PDO execute() behavior of zero-indexed arrays + if (isset($values[0])) { + array_unshift($values, null); + } + // rebuild the statement and values - $rebuilder = new Rebuilder($this); - list($statement, $values) = $rebuilder->__invoke($statement, $values); + list($statement, $values) = $this->rebuild($statement, $values); // prepare the statement $sth = $this->prepare($statement); @@ -929,4 +928,210 @@ public function prepareWithValues($statement, array $values = array()) // done return $sth; } + + /** + * + * Returns a new anonymous object to track bind values. + * + * @param array $values The values to bind and/or replace into a statement. + * + * @return object + * + */ + protected function newBindTracker($values) + { + // anonymous object to track preparation info + return (object) array( + // how many numbered placeholders in the original statement + 'num' => 0, + // how many numbered placeholders to actually be bound; this may + // differ from 'num' in that some numbered placeholders may get + // replaced with quoted CSV strings + 'count' => 0, + // initial values to be bound + 'values' => $values, + // named and numbered placeholders to bind at the end + 'final_values' => array(), + ); + } + + /** + * + * Rebuilds a statement with array values replaced into placeholders. + * + * @param string $statement The statement to rebuild. + * + * @param array $values The values to bind and/or replace into a statement. + * + * @return array An array where element 0 is the rebuilt statement and + * element 1 is the rebuilt array of values. + * + */ + protected function rebuild($statement, $values) + { + $bind = $this->newBindTracker($values); + $statement = $this->rebuildStatement($statement, $bind); + return array($statement, $bind->final_values); + } + + /** + * + * Given a statement, rebuilds it with array values embedded. + * + * @param string $statement The SQL statement. + * + * @param object $bind The bind-values tracker. + * + * @return string The rebuilt statement. + * + */ + protected function rebuildStatement($statement, $bind) + { + // find all parts not inside quotes or backslashed-quotes + $apos = "'"; + $quot = '"'; + $parts = preg_split( + "/(($apos+|$quot+|\\$apos+|\\$quot+).*?)\\2/m", + $statement, + -1, + PREG_SPLIT_DELIM_CAPTURE + ); + return $this->rebuildParts($parts, $bind); + } + + /** + * + * Given an array of statement parts, rebuilds each part. + * + * @param array $parts The statement parts. + * + * @param object $bind The bind-values tracker. + * + * @return string The rebuilt statement. + * + */ + protected function rebuildParts($parts, $bind) + { + // loop through the non-quoted parts (0, 3, 6, 9, etc.) + $k = count($parts); + for ($i = 0; $i <= $k; $i += 3) { + $parts[$i] = $this->rebuildPart($parts[$i], $bind); + } + return implode('', $parts); + } + + /** + * + * Rebuilds a single statement part. + * + * @param string $part The statement part. + * + * @param object $bind The bind-values tracker. + * + * @return string The rebuilt statement. + * + */ + protected function rebuildPart($part, $bind) + { + // split into subparts by ":name" and "?" + $subs = preg_split( + "/(:[a-zA-Z_][a-zA-Z0-9_]*)|(\?)/m", + $part, + -1, + PREG_SPLIT_DELIM_CAPTURE + ); + + // check subparts to convert bound arrays to quoted CSV strings + $subs = $this->prepareValuePlaceholders($subs, $bind); + + // reassemble + return implode('', $subs); + } + + /** + * + * Prepares the sub-parts of a query with placeholders. + * + * @param array $subs The query subparts. + * + * @param object $bind The preparation info object. + * + * @return array The prepared subparts. + * + */ + protected function prepareValuePlaceholders(array $subs, $bind) + { + foreach ($subs as $i => $sub) { + $char = substr($sub, 0, 1); + if ($char == '?') { + $subs[$i] = $this->prepareNumberedPlaceholder($sub, $bind); + } + + if ($char == ':') { + $subs[$i] = $this->prepareNamedPlaceholder($sub, $bind); + } + } + + return $subs; + } + + /** + * + * Bind or quote a numbered placeholder in a query subpart. + * + * @param string $sub The query subpart. + * + * @param object $bind The preparation info object. + * + * @return string The prepared query subpart. + * + */ + protected function prepareNumberedPlaceholder($sub, $bind) + { + // what numbered placeholder is this in the original statement? + $bind->num ++; + + // is the corresponding data element an array? + $bind_array = isset($bind->values[$bind->num]) + && is_array($bind->values[$bind->num]); + if ($bind_array) { + // PDO won't bind an array; quote and replace directly + $sub = $this->quote($bind->values[$bind->num]); + } else { + // increase the count of numbered placeholders to be bound + $bind->count ++; + $bind->final_values[$bind->count] = $bind->values[$bind->num]; + } + + return $sub; + } + + /** + * + * Bind or quote a named placeholder in a query subpart. + * + * @param string $sub The query subpart. + * + * @param object $bind The preparation info object. + * + * @return string The prepared query subpart. + * + */ + protected function prepareNamedPlaceholder($sub, $bind) + { + $name = substr($sub, 1); + + // is the corresponding data element an array? + $bind_array = isset($bind->values[$name]) + && is_array($bind->values[$name]); + if ($bind_array) { + // PDO won't bind an array; quote and replace directly + $sub = $this->quote($bind->values[$name]); + } else { + // not an array, retain the placeholder for later + $bind->final_values[$name] = $bind->values[$name]; + } + + return $sub; + } }