Skip to content

Potential improvement: add support for custom PDO implementation #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.idea/
/vendor/
/composer.lock
composer.phar
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove this if needed but seemed sensible to include it

67 changes: 56 additions & 11 deletions src/Codeception/Lib/Driver/Db.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,36 @@ class Db
* @var array
*/
protected $primaryKeys = [];
/**
* @var string|null
*/
protected $pdo_class;

/**
* @param string|null $pdo_class
* @return string
*/
private static function pdoClass($pdo_class){
if (!$pdo_class){
// If empty or null we use regular PDO
return \PDO::class;
}

if (!class_exists($pdo_class)){
throw new ModuleException(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially this returned the fallback value of \PDO::class but actually throwing an exception is more helpful to the user - they can resolve by correcting the class name or by simply removing the config item.

'Codeception\Module\Db',
"The class with provided config value 'pdo_class' ($pdo_class) does not exist"
);
}

public static function connect($dsn, $user, $password, $options = null)
return $pdo_class;
}

public static function connect($dsn, $user, $password, $options = null, $pdo_class = null)
{
$dbh = new \PDO($dsn, $user, $password, $options);
$class_name = self::pdoClass($pdo_class);
$dbh = new $class_name($dsn, $user, $password, $options);
self::assertIsPdo($dbh, $pdo_class);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert is necessary so we don't get something that accepts the right arguments but doesn't provided expected methods and/or fails expected typehints down the line.

$dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

return $dbh;
Expand All @@ -48,31 +74,32 @@ public static function connect($dsn, $user, $password, $options = null)
* @param $user
* @param $password
* @param [optional] $options
* @param [optional] $pdo_class
*
* @see http://php.net/manual/en/pdo.construct.php
* @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants
*
* @return Db|SqlSrv|MySql|Oci|PostgreSql|Sqlite
*/
public static function create($dsn, $user, $password, $options = null)
public static function create($dsn, $user, $password, $options = null, $pdo_class = null)
{
$provider = self::getProvider($dsn);

switch ($provider) {
case 'sqlite':
return new Sqlite($dsn, $user, $password, $options);
return new Sqlite($dsn, $user, $password, $options, $pdo_class);
case 'mysql':
return new MySql($dsn, $user, $password, $options);
return new MySql($dsn, $user, $password, $options, $pdo_class);
case 'pgsql':
return new PostgreSql($dsn, $user, $password, $options);
return new PostgreSql($dsn, $user, $password, $options, $pdo_class);
case 'mssql':
case 'dblib':
case 'sqlsrv':
return new SqlSrv($dsn, $user, $password, $options);
return new SqlSrv($dsn, $user, $password, $options, $pdo_class);
case 'oci':
return new Oci($dsn, $user, $password, $options);
return new Oci($dsn, $user, $password, $options, $pdo_class);
default:
return new Db($dsn, $user, $password, $options);
return new Db($dsn, $user, $password, $options, $pdo_class);
}
}

Expand All @@ -86,19 +113,37 @@ public static function getProvider($dsn)
* @param $user
* @param $password
* @param [optional] $options
* @param [optional] $pdo_class
*
* @see http://php.net/manual/en/pdo.construct.php
* @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants
*/
public function __construct($dsn, $user, $password, $options = null)
public function __construct($dsn, $user, $password, $options = null, $pdo_class = null)
{
$this->dbh = new \PDO($dsn, $user, $password, $options);
$class_name = self::pdoClass($pdo_class);
$this->dbh = new $class_name($dsn, $user, $password, $options);
self::assertIsPdo($this->dbh, $pdo_class);
$this->dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

$this->dsn = $dsn;
$this->user = $user;
$this->password = $password;
$this->options = $options;
$this->pdo_class = $pdo_class;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing this the same as the rest so it can be passed back in if necessary (e.g. sqllite reconnection)

}

/**
* @param $dbh
* @param string|null $pdo_class
*/
private static function assertIsPdo($dbh, $pdo_class)
{
if (!$dbh instanceof \PDO){
throw new ModuleException(
'Codeception\Module\Db',
"The provided config value 'pdo_class' ($pdo_class) did not resolve to a class that implements \\PDO"
);
}
}

public function __destruct()
Expand Down
6 changes: 3 additions & 3 deletions src/Codeception/Lib/Driver/Sqlite.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Sqlite extends Db
protected $filename = '';
protected $con = null;

public function __construct($dsn, $user, $password, $options = null)
public function __construct($dsn, $user, $password, $options = null, $pdo_class = null)
{
$filename = substr($dsn, 7);
if ($filename === ':memory:') {
Expand All @@ -19,15 +19,15 @@ public function __construct($dsn, $user, $password, $options = null)

$this->filename = Configuration::projectDir() . $filename;
$this->dsn = 'sqlite:' . $this->filename;
parent::__construct($this->dsn, $user, $password, $options);
parent::__construct($this->dsn, $user, $password, $options, $pdo_class);
}

public function cleanup()
{
$this->dbh = null;
gc_collect_cycles();
file_put_contents($this->filename, '');
$this->dbh = self::connect($this->dsn, $this->user, $this->password);
$this->dbh = self::connect($this->dsn, $this->user, $this->password, $this->pdo_class);
}

public function load($sql)
Expand Down
4 changes: 3 additions & 1 deletion src/Codeception/Module/Db.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
* * ssl_cipher - list of one or more permissible ciphers to use for SSL encryption (MySQL specific, @see http://php.net/manual/de/ref.pdo-mysql.php#pdo.constants.mysql-attr-cipher)
* * databases - include more database configs and switch between them in tests.
* * initial_queries - list of queries to be executed right after connection to the database has been initiated, i.e. creating the database if it does not exist or preparing the database collation
* * pdo_class - a fully qualified class name of a class which extends \PDO. This allows for custom stubbing of PDO to provide customised interaction with the database, or alternative implementations which may aid in test debugging or test speed
*
* ## Example
*
Expand Down Expand Up @@ -570,7 +571,8 @@ private function connect($databaseKey, $databaseConfig)

try {
$this->debugSection('Connecting To Db', ['config' => $databaseConfig, 'options' => $options]);
$this->drivers[$databaseKey] = Driver::create($databaseConfig['dsn'], $databaseConfig['user'], $databaseConfig['password'], $options);
$pdo_class = array_key_exists('pdo_class', $databaseConfig) ? $databaseConfig['pdo_class'] : null;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a bit more bulk but without it a lot of tests fail due to notices.

$this->drivers[$databaseKey] = Driver::create($databaseConfig['dsn'], $databaseConfig['user'], $databaseConfig['password'], $options, $pdo_class);
} catch (\PDOException $e) {
$message = $e->getMessage();
if ($message === 'could not find driver') {
Expand Down
Binary file modified tests/data/sqlite.db
Binary file not shown.