Skip to content

Instantly share code, notes, and snippets.

@christiaangoossens
Forked from cjthompson/RobustPDO.php
Last active December 8, 2019 14:01
Show Gist options
  • Save christiaangoossens/6fb1720bf854e6a79d630a8f9e7af66e to your computer and use it in GitHub Desktop.
Save christiaangoossens/6fb1720bf854e6a79d630a8f9e7af66e to your computer and use it in GitHub Desktop.
Extended PDO class that detects dropped connections and reconnects on all things that Eloquent ORM would also reconnect on.
<?php
class RobustPDO extends PDO
{
/** Call setAttribute to set the session wait_timeout value */
const ATTR_MYSQL_TIMEOUT = 100;
/** @var array */
protected $config = [];
/** @var bool For lazy connection tracking */
protected $_connected = false;
/** @var array Cached attributes for reconnection */
private $attributes = [];
/**
* Create a new PDO object.
* Does not connect to the database until needed.
*
* @param string $dsn The Data Source Name, or DSN, contains the information required to connect to the database.
* @param string $user [optional] The user name for the DSN string. This parameter is optional for some PDO drivers.
* @param string $pass [optional] The password for the DSN string. This parameter is optional for some PDO drivers.
* @param string $options [optional] A key=>value array of driver-specific connection options.
*/
public function __construct($dsn, $user = null, $pass = null, $options = null)
{
//Save connection details for later
$this->config = array(
'dsn' => $dsn,
'user' => $user,
'pass' => $pass,
'options' => $options
);
if (!is_array($this->config['options'])) {
$this->config['options'] = [];
}
// Throw exceptions when there's an error so we can catch them
$this->config['options'][PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
$this->config['options'][PDO::ATTR_DEFAULT_FETCH_MODE] = PDO::FETCH_OBJ;
if (isset($this->config['options'][self::ATTR_MYSQL_TIMEOUT])) {
$this->attributes[self::ATTR_MYSQL_TIMEOUT] = $this->config['options'][self::ATTR_MYSQL_TIMEOUT];
}
}
/**
* Verifies that the PDO connection is still active. If not, it reconnects.
*/
public function reconnect()
{
parent::__construct($this->config['dsn'], $this->config['user'], $this->config['pass'], $this->config['options']);
// Reapply attributes to the new connection
foreach ($this->attributes as $attr => $value) {
$this->_setAttribute($attr, $value);
}
$this->_connected = true;
}
/**
* Try to call the function on the parent inside a try/catch to detect a disconnect
*
* @throws PDOException
*/
public function __call($name, $arguments)
{
if (!$this->_connected) {
$this->reconnect();
}
try {
return call_user_func_array(['parent', $name], $arguments);
} catch (PDOException $e) {
if (static::hasGoneAway($e)) {
$this->reconnect();
return call_user_func_array(['parent', $name], $arguments);
} else {
throw $e;
}
}
}
/**
* {@InheritDoc}
*/
public function beginTransaction()
{
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* {@InheritDoc}
*/
public function query($statement)
{
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* {@InheritDoc}
*/
public function exec($statement)
{
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* {@InheritDoc}
*/
public function getAttribute($statement)
{
return $this->__call(__FUNCTION__, func_get_args());
}
/**
* {@InheritDoc}
*/
public function prepare($statement, $driver_options = null)
{
if (!$this->_connected) {
$this->reconnect();
}
if (is_null($driver_options)) {
$driver_options = [];
}
try {
return parent::prepare($statement, $driver_options);
} catch (PDOException $e) {
if (static::hasGoneAway($e)) {
$this->reconnect();
return parent::prepare($statement, $driver_options);
} else {
throw $e;
}
}
}
/**
* {@InheritDoc}
*
* Caches setAttribute calls to be reapplied on reconnect
*/
public function setAttribute($attribute, $value)
{
$this->attributes[$attribute] = $value;
if ($this->_connected) {
try {
$this->_setAttribute($attribute, $value);
} catch (PDOException $e) {
if (static::hasGoneAway($e)) {
$this->reconnect();
} else {
throw $e;
}
}
}
return true;
}
/**
* Executes a prepared statement inside a try/catch to watch for server disconnections
*
* Use to detect connection drops between prepare and PDOStatement->execute
* Usage:
* <code>
* $st = $this->robustPDO->prepare('SELECT :one');
* if ($this->robustPDO->tryExecuteStatement($st, ['one'=>1])) { print_r($st->fetchAll()); }
* </code>
*
* @param PDOStatement $st PDOStatement to execute
* @param null|array $input_parameters An array of values with as many elements as there are bound parameters in the SQL statement
* @param bool $recursion If TRUE, don't retry the operation
* @throws PDOException If the statement throws an exception that's not a disconnect
* @return bool Returns TRUE on success or FALSE on failure.
*/
public function tryExecuteStatement(PDOStatement &$st, $input_parameters = null, $recursion = false)
{
try {
if (is_array($input_parameters)) {
return $st->execute($input_parameters);
} else {
return $st->execute();
}
} catch (PDOException $e) {
if (!$recursion && static::hasGoneAway($e)) {
$this->reconnect();
$st = $this->prepare($st->queryString);
return $this->tryExecuteStatement($st, $input_parameters, true);
}
throw $e;
}
}
/**
* Check if a PDOException is a server disconnection or not
*
* @param PDOException $e
* @return bool Returns TRUE if the PDOException is any of the specified connection errors or FALSE for another error
*/
public static function hasGoneAway(PDOException $e)
{
$errors = [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'ORA-03114',
'Packets out of order. Expected',
'Adaptive Server connection failed',
'Communication link failure',
'connection is no longer usable',
'Login timeout expired',
];
foreach ($errors as $error) {
if (stristr($e->getMessage(), $error)) {
return true;
}
}
return false;
}
/**
* Ping the PDO connection to keep it alive by sending a SELECT 1
*
* @return bool
*/
public function ping()
{
return (1 === intval($this->query('SELECT 1')->fetchColumn(0)));
}
/**
* Sets a connection attribute. Adds support for additional custom attributes from this class.
*
* This calls setAttribute right away and requires that the PDO connection be open.
*
* @param int $attribute
* @param mixed $value
* @return bool Returns TRUE on success or FALSE on failure
*/
protected function _setAttribute($attribute, $value)
{
if ($attribute === self::ATTR_MYSQL_TIMEOUT) {
parent::exec("SET SESSION wait_timeout=" . intval($value));
return true;
} else {
return parent::setAttribute($attribute, $value);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment