Skip to content

Commit

Permalink
Repair param,value and column binding;
Browse files Browse the repository at this point in the history
Add tests;
Modify exception tests;
Remove unnecessary test bootstrap.php
  • Loading branch information
adamturcsan committed Nov 6, 2016
1 parent 61adfaa commit 7a357cc
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 77 deletions.
2 changes: 1 addition & 1 deletion src/ReconnectingPDO.php
Expand Up @@ -128,7 +128,7 @@ protected function call($method, $arguments)
}
}
if($returnValue instanceof \PDOStatement) {
return new ReconnectingPDOStatement($returnValue, $this->db, $this->maxReconnection);
return new ReconnectingPDOStatement($returnValue, $this, $this->maxReconnection);
}
return $returnValue;
}
Expand Down
118 changes: 76 additions & 42 deletions src/ReconnectingPDOStatement.php
Expand Up @@ -9,13 +9,19 @@

namespace Legow\ReconnectingPDO;

use \PDO;
use Legow\ReconnectingPDO\ReconnectingPDO;
use \PDOStatement;

/**
* Description of ReconnectingPDOStatement
*
* @author Turcsán Ádám <turcsan.adam@legow.hu>
* @method bool execute(array $parameters = null [optional]) Executes a prepared statement
* @method mixed fetch(int $fetchType = null [optional], int $cursor_orientation = PDO::FETCH_ORI_NEXT [optional], int $cursor_offset = 0 [optional]) Fetches the next row from a result set
* @method bool bindParam(mixed $parameter, mixed &$variable, int $dataType = PDO::PARAM_STR [optional], int $length = null [optional], $driver_options = null [optional]) Binds a parameter to the specified variable name
* @method bool bindColumn(mixed $column, mixed &$param, int $type = null [optional], int $maxlen = null [optional], $driverdata = null [optional]) Bind a column to a PHP variable
* @method bool bindValue(mixed $parameter, mixed $value, int $data_type = PDO::PARAM_STR [optional]) Binds a value to a parameter
* @method int rowCount() Returns the number of rows affected by the last SQL statement
*/
class ReconnectingPDOStatement
{
Expand All @@ -25,20 +31,10 @@ class ReconnectingPDOStatement
private $statement;

/**
* @var PDO
* @var ReconnectingPDO
*/
private $connection;

/**
* @var int Reconnect counter
*/
protected $reconnectCounter;

/**
* @var int Maximum reconnection for one function call
*/
protected $maxReconnection;

/**
* @var string
*/
Expand All @@ -47,81 +43,119 @@ class ReconnectingPDOStatement
/**
* @var array
*/
protected $seedData;
protected $seedData = [];

/**
* @var bool
*/
protected $executed = false;

/**
*
* @param PDOStatement $statement
* @param int $maxRetry [optional]
* @param ReconnectingPDO $connection
*/
public function __construct(PDOStatement $statement, PDO $connection, $maxRetry = 5)
public function __construct(PDOStatement $statement, ReconnectingPDO $connection)
{
$this->statement = $statement;
$this->queryString = $statement->queryString;
$this->connection = $connection;
$this->maxReconnection = $maxRetry;
}

/**
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public function __call($method, $arguments)
{
if(substr($method, 0, 4) == "bind") {
$this->seedData[$method][array_shift($ärguments)] = $arguments;
if(substr($method, 0, 4) == 'bind' && $method != 'bindColumn') {
$key = $arguments[0];
$this->seedData[$method][$key] = $arguments;
return $this->call($method, $this->seedData[$method][$key]);
} elseif( $method == 'execute' ) {
$this->executed = true;
}
return $this->call($method, $arguments); // Avoid direct calling of magic method
}

public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null)
{
$this->seedData['bindColumn'][$column] = &$param;
return $this->statement->bindColumn($column, $param, $type, $maxlen, $driverdata);
}

/**
* @param string $method
* @param array $arguments
* @return mixed
* @throws \PDOException
*/
protected function call($method, $arguments)
protected function call($method, &$arguments)
{
try {
if($method == 'bindParam'){
return $this->statement->bindParam(...$arguments);
}
return call_user_func_array([$this->statement, $method], $arguments);
} catch (\PDOException $ex) {
if (!stristr($ex->getMessage(), "server has gone away") || $ex->getCode() != 'HY000') {
throw $ex;
}
if ($this->reconnectCounter < $this->maxReconnection) {
$this->recreateStatement();
$returnValue = $this->call($method, $arguments); // Retry
$this->resetCounter();
return $returnValue;
} else {
throw $ex;
}
$this->recreateStatement();
$returnValue = $this->call($method, $arguments); // Retry
return $returnValue;
}
}

protected function recreateStatement() {
$statement = $this->connection->prepare($this->queryString);
$shouldBeExecuted = $this->executed;
/* @var $reconnectingstatement ReconnectingPDOStatement */
$reconnectingstatement = $this->connection->prepare($this->queryString);
$this->executed = false;
$statement = $reconnectingstatement->getPDOStatement();
if(!empty($this->seedData)) {
foreach($this->seedData as $key => $valueData) {
/* @var $$key string stores bind method (bindValue or bindParam) */
/* @var $valueData array stores a value and a paramtype */
list($value, $paramType) = $valueData;
$statement->$key($value, $paramType);
/* @var $method string bindParam, bindColumn or bindValue*/
foreach($this->seedData as $method => $arguments) {
/* @var $key string Parameter name */
foreach($arguments as $key => $params) {
list($name, , $paramType) = $params; // Value comes from the seedData array, because it only takes it by reference
$statement->$key($name, $this->seedData[$method][$key][1], $paramType);
}
}
}
$this->statement = $statement;
if($shouldBeExecuted) {
$this->execute();
}
}

/**
* If a function call didn't throw exception, reconnection counter can be reseted
* @return \PDOStatement
*/
protected function resetCounter()
public function getPDOStatement()
{
$this->reconnectCounter = 0;
return $this->statement;
}

/**
*
* @param int $max
* @return bool
*/
public function setMaxReconnection($max)
public function isExecuted()
{
return $this->executed;
}

public function fetch()
{
$this->maxReconnection = $max;
$args = func_get_args();
$result = $this->call('fetch', $args);
if(isset($this->seedData['bindColumn']) && count($this->seedData['bindColumn'])) {
foreach($this->seedData['bindColumn'] as $name => &$column) {
$this->seedData['bindColumn'][$name] = $result[$name];
}
}
return $result;
}
}
80 changes: 79 additions & 1 deletion test/ReconnectingPDOStatementTest.php
Expand Up @@ -23,6 +23,9 @@
class ReconnectingPDOStatementTest extends TestCase
{

/**
* @var string
*/
protected $testDSN = 'sqlite::memory:';

public function testConstruct()
Expand All @@ -37,11 +40,86 @@ public function testConstruct()
$this->assertInstanceOf(\TypeError::class, $error);
}
$pdo = new PDO($this->testDSN);
$rpdo = new ReconnectingPDO();
$rpdo->setConnectionParameters([
"dsn" => $this->testDSN,
"passwd" => '',
"username" => ''
]);
$rpdo->setPDO($pdo);
$stm = $pdo->prepare('SELECT 1');

$rstm = new ReconnectingPDOStatement($stm, $pdo);
$rstm = new ReconnectingPDOStatement($stm, $rpdo);
$this->assertInstanceOf(ReconnectingPDOStatement::class, $rstm);

}

/**
*/
public function testBinding()
{
$pdo = new ReconnectingPDO($this->testDSN);
$rstm = $pdo->prepare('SELECT :param, :value, 1 as column;');
$param = 'test';
$rstm->bindValue('value','value',PDO::PARAM_STR);
$rstm->bindParam('param', $param, PDO::PARAM_STR);

$this->assertTrue($rstm->execute());
$this->assertEquals([$param, 'value', 1], $rstm->fetch(PDO::FETCH_NUM));

$value = $id = null;
$statement = $pdo->prepare('SELECT 11, "valuevalue" as value;');
$statement->bindColumn('value', $value);

$statement->execute();

$row = $statement->fetch(PDO::FETCH_ASSOC);

$reflection = new \ReflectionClass($statement);
$prop = $reflection->getProperty('seedData');
$prop->setAccessible(true);
$seedData = $prop->getValue($statement);
$this->assertEquals('valuevalue', $value);
}

public function testGetPDOStatement()
{
$mockStatement = $this->createMock(PDOStatement::class);
$mockRPDO = $this->createMock(ReconnectingPDO::class);

$rstm = new ReconnectingPDOStatement($mockStatement, $mockRPDO);

$this->assertInstanceOf(PDOStatement::class, $rstm->getPDOStatement());
$this->assertEquals($mockStatement, $rstm->getPDOStatement());
}

public function testRecreation()
{
// Create failing statement while fecthing
$mockStm = $this->createMock(PDOStatement::class);
$mockStm->method('fetch')->will($this->throwException(new \PDOException('Mysql server has gone away')));
$mockStm->method('execute')->willReturn(true);

// Create reconnecting PDO
$rPDO = new ReconnectingPDO($this->testDSN, '', '');
$pdo = $this->createMock(PDO::class);
$pdo->method('prepare')->will($this->throwException(new \PDOException('Mysql server has gone away')));
$rPDO->setPDO($pdo);

$rstm = new ReconnectingPDOStatement($mockStm, $rPDO);

// Set query string to have working querystring while recreation
$reflection = new \ReflectionClass($rstm);
$queryStringProperty = $reflection->getProperty('queryString');
$queryStringProperty->setAccessible(true);
$queryStringProperty->setValue($rstm, 'SELECT 1;');

$this->assertFalse($rstm->isExecuted());
$rstm->execute();
$this->assertTrue($rstm->isExecuted());
$this->assertEquals("1", $rstm->fetch(PDO::FETCH_COLUMN), "First fetch try which triggers reconnection");

$this->assertNotEquals($mockStm, $rstm->getPDOStatement(), "Check if statement has been recreated");
$this->assertTrue($rstm->isExecuted(), "Check if recreated");
}
}
31 changes: 12 additions & 19 deletions test/ReconnectingPDOTest.php
Expand Up @@ -124,6 +124,10 @@ public function testReconnection()
$this->assertInstanceOf(ExceededMaxReconnectionException::class, $exception);
}

/**
* @expectedException \PDOException
* @expectedExceptionMessage Test exception
*/
public function testExceptionThrowUp()
{
$mockPDO = $this->createMock(\PDO::class);
Expand All @@ -133,31 +137,20 @@ public function testExceptionThrowUp()

$rpdo = new ReconnectingPDO('sqlite::memory:', '', '');
$rpdo->setPDO($mockPDO);

$exception = null;
try {
$rpdo->prepare('SELECT 1');
} catch (\Exception $ex) {
$exception = $ex;
}
$this->assertInstanceOf(\PDOException::class, $exception);
$this->assertContains('Test exception', $exception->getMessage());
//Should throw exception
$rpdo->prepare('SELECT 1');
}

/**
* @expectedException \Legow\ReconnectingPDO\ReconnectingPDOException
* @expectedExceptionMessage No PDO connection
*/
public function testCallProtection()
{
$rpdo = new ReconnectingPDO();

$exception = null;
try {
$rpdo->prepare('SELECT 1');
} catch (\Exception $ex) {
$exception = $ex;
}
$this->assertInstanceOf(ReconnectingPDOException::class, $exception);
$this->assertContains('No PDO connection', $exception->getMessage());
//Should throw exception
$rpdo->prepare('SELECT 1');
}



}
14 changes: 0 additions & 14 deletions test/bootstrap.php

This file was deleted.

0 comments on commit 7a357cc

Please sign in to comment.