seeInDatabase('users', ['name' => 'Davert', 'email' => 'davert@mail.com']); * * ``` * Will generate: * * ```sql * SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` = 'davert@mail.com' * ``` * Since version 2.1.9 it's possible to use LIKE in a condition, as shown here: * * ```php * seeInDatabase('users', ['name' => 'Davert', 'email like' => 'davert%']); * * ``` * Will generate: * * ```sql * SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` LIKE 'davert%' * ``` * ## Public Properties * * dbh - contains the PDO connection * * driver - contains the Connection Driver * */ class Db extends CodeceptionModule implements DbInterface { /** * @var array */ protected $config = [ 'populate' => false, 'cleanup' => false, 'reconnect' => false, 'waitlock' => 0, 'dump' => null, 'populator' => null, ]; /** * @var array */ protected $requiredFields = ['dsn', 'user', 'password']; const DEFAULT_DATABASE = 'default'; /** * @var Driver[] */ public $drivers = []; /** * @var \PDO[] */ public $dbhs = []; public $databasesPopulated = []; public $databasesSql = []; protected $insertedRows = []; public $currentDatabase = self::DEFAULT_DATABASE; protected function getDatabases() { $databases = [$this->currentDatabase => $this->config]; if (!empty($this->config['databases'])) { foreach ($this->config['databases'] as $databaseKey => $databaseConfig) { $databases[$databaseKey] = array_merge([ 'populate' => false, 'cleanup' => false, 'reconnect' => false, 'waitlock' => 0, 'dump' => null, 'populator' => null, ], $databaseConfig); } } return $databases; } protected function connectToDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->connect($databaseKey, $databaseConfig); } } protected function cleanUpDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->_cleanup($databaseKey, $databaseConfig); } } protected function populateDatabases($configKey) { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { if ($databaseConfig[$configKey]) { if (!$databaseConfig['populate']) { return; } if (isset($this->databasesPopulated[$databaseKey]) && $this->databasesPopulated[$databaseKey]) { return; } $this->_loadDump($databaseKey, $databaseConfig); } } } protected function readSqlForDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->readSql($databaseKey, $databaseConfig); } } protected function removeInsertedForDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->amConnectedToDatabase($databaseKey); $this->removeInserted($databaseKey); } } protected function disconnectDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { $this->disconnect($databaseKey); } } protected function reconnectDatabases() { foreach ($this->getDatabases() as $databaseKey => $databaseConfig) { if ($databaseConfig['reconnect']) { $this->disconnect($databaseKey); $this->connect($databaseKey, $databaseConfig); } } } public function __get($name) { Notification::deprecate("Properties dbh and driver are deprecated in favor of Db::_getDbh and Db::_getDriver", "Db module"); if ($name == 'driver') { return $this->_getDriver(); } if ($name == 'dbh') { return $this->_getDbh(); } } /** * @return Driver */ public function _getDriver() { return $this->drivers[$this->currentDatabase]; } public function _getDbh() { return $this->dbhs[$this->currentDatabase]; } /** * Make sure you are connected to the right database. * * ```php * seeNumRecords(2, 'users'); //executed on default database * $I->amConnectedToDatabase('db_books'); * $I->seeNumRecords(30, 'books'); //executed on db_books database * //All the next queries will be on db_books * ``` * @param $databaseKey * @throws ModuleConfigException */ public function amConnectedToDatabase($databaseKey) { if (empty($this->getDatabases()[$databaseKey]) && $databaseKey != self::DEFAULT_DATABASE) { throw new ModuleConfigException( __CLASS__, "\nNo database $databaseKey in the key databases.\n" ); } $this->currentDatabase = $databaseKey; } /** * Can be used with a callback if you don't want to change the current database in your test. * * ```php * seeNumRecords(2, 'users'); //executed on default database * $I->performInDatabase('db_books', function($I) { * $I->seeNumRecords(30, 'books'); //executed on db_books database * }); * $I->seeNumRecords(2, 'users'); //executed on default database * ``` * List of actions can be pragmatically built using `Codeception\Util\ActionSequence`: * * ```php * performInDatabase('db_books', ActionSequence::build() * ->seeNumRecords(30, 'books') * ); * ``` * Alternatively an array can be used: * * ```php * $I->performInDatabase('db_books', ['seeNumRecords' => [30, 'books']]); * ``` * * Choose the syntax you like the most and use it, * * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to * exception on failure. * * @param $databaseKey * @param \Codeception\Util\ActionSequence|array|callable $actions * @throws ModuleConfigException */ public function performInDatabase($databaseKey, $actions) { $backupDatabase = $this->currentDatabase; $this->amConnectedToDatabase($databaseKey); if (is_callable($actions)) { $actions($this); $this->amConnectedToDatabase($backupDatabase); return; } if (is_array($actions)) { $actions = ActionSequence::build()->fromArray($actions); } if (!$actions instanceof ActionSequence) { throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array"); } $actions->run($this); $this->amConnectedToDatabase($backupDatabase); } public function _initialize() { $this->connectToDatabases(); } public function __destruct() { $this->disconnectDatabases(); } public function _beforeSuite($settings = []) { $this->readSqlForDatabases(); $this->connectToDatabases(); $this->cleanUpDatabases(); $this->populateDatabases('populate'); } private function readSql($databaseKey = null, $databaseConfig = null) { if ($databaseConfig['populator']) { return; } if (!$databaseConfig['cleanup'] && !$databaseConfig['populate']) { return; } if (empty($databaseConfig['dump'])) { return; } if (!is_array($databaseConfig['dump'])) { $databaseConfig['dump'] = [$databaseConfig['dump']]; } $sql = ''; foreach ($databaseConfig['dump'] as $filePath) { $sql .= $this->readSqlFile($filePath); } if (!empty($sql)) { // split SQL dump into lines $this->databasesSql[$databaseKey] = preg_split('/\r\n|\n|\r/', $sql, -1, PREG_SPLIT_NO_EMPTY); } } /** * @param $filePath * * @return bool|null|string|string[] * @throws \Codeception\Exception\ModuleConfigException */ private function readSqlFile($filePath) { if (!file_exists(Configuration::projectDir() . $filePath)) { throw new ModuleConfigException( __CLASS__, "\nFile with dump doesn't exist.\n" . "Please, check path for sql file: " . $filePath ); } $sql = file_get_contents(Configuration::projectDir() . $filePath); // remove C-style comments (except MySQL directives) $sql = preg_replace('%/\*(?!!\d+).*?\*/%s', '', $sql); return $sql; } private function connect($databaseKey, $databaseConfig) { if (!empty($this->drivers[$databaseKey]) && !empty($this->dbhs[$databaseKey])) { return; } $options = []; /** * @see http://php.net/manual/en/pdo.construct.php * @see http://php.net/manual/de/ref.pdo-mysql.php#pdo-mysql.constants */ if (array_key_exists('ssl_key', $databaseConfig) && !empty($databaseConfig['ssl_key']) && defined('\PDO::MYSQL_ATTR_SSL_KEY') ) { $options[\PDO::MYSQL_ATTR_SSL_KEY] = (string) $databaseConfig['ssl_key']; } if (array_key_exists('ssl_cert', $databaseConfig) && !empty($databaseConfig['ssl_cert']) && defined('\PDO::MYSQL_ATTR_SSL_CERT') ) { $options[\PDO::MYSQL_ATTR_SSL_CERT] = (string) $databaseConfig['ssl_cert']; } if (array_key_exists('ssl_ca', $databaseConfig) && !empty($databaseConfig['ssl_ca']) && defined('\PDO::MYSQL_ATTR_SSL_CA') ) { $options[\PDO::MYSQL_ATTR_SSL_CA] = (string) $databaseConfig['ssl_ca']; } if (array_key_exists('ssl_cipher', $databaseConfig) && !empty($databaseConfig['ssl_cipher']) && defined('\PDO::MYSQL_ATTR_SSL_CIPHER') ) { $options[\PDO::MYSQL_ATTR_SSL_CIPHER] = (string) $databaseConfig['ssl_cipher']; } if (array_key_exists('ssl_verify_server_cert', $databaseConfig) && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') ) { $options[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (boolean) $databaseConfig[ 'ssl_verify_server_cert' ]; } try { $this->debugSection('Connecting To Db', ['config' => $databaseConfig, 'options' => $options]); $this->drivers[$databaseKey] = Driver::create($databaseConfig['dsn'], $databaseConfig['user'], $databaseConfig['password'], $options); } catch (\PDOException $e) { $message = $e->getMessage(); if ($message === 'could not find driver') { list ($missingDriver, ) = explode(':', $databaseConfig['dsn'], 2); $message = "could not find $missingDriver driver"; } throw new ModuleException(__CLASS__, $message . ' while creating PDO connection'); } if ($databaseConfig['waitlock']) { $this->_getDriver()->setWaitLock($databaseConfig['waitlock']); } if (isset($databaseConfig['initial_queries'])) { foreach ($databaseConfig['initial_queries'] as $initialQuery) { $this->drivers[$databaseKey]->executeQuery($initialQuery, []); } } $this->debugSection('Db', 'Connected to ' . $databaseKey . ' ' . $this->drivers[$databaseKey]->getDb()); $this->dbhs[$databaseKey] = $this->drivers[$databaseKey]->getDbh(); } private function disconnect($databaseKey) { $this->debugSection('Db', 'Disconnected from ' . $databaseKey); $this->dbhs[$databaseKey] = null; $this->drivers[$databaseKey] = null; } public function _before(TestInterface $test) { $this->reconnectDatabases(); $this->amConnectedToDatabase(self::DEFAULT_DATABASE); $this->cleanUpDatabases(); $this->populateDatabases('cleanup'); parent::_before($test); } public function _after(TestInterface $test) { $this->removeInsertedForDatabases(); parent::_after($test); } protected function removeInserted($databaseKey = null) { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; if (empty($this->insertedRows[$databaseKey])) { return; } foreach (array_reverse($this->insertedRows[$databaseKey]) as $row) { try { $this->_getDriver()->deleteQueryByCriteria($row['table'], $row['primary']); } catch (\Exception $e) { $this->debug("Couldn't delete record " . json_encode($row['primary']) ." from {$row['table']}"); } } $this->insertedRows[$databaseKey] = []; } public function _cleanup($databaseKey = null, $databaseConfig = null) { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; if (!$databaseConfig['populate']) { return; } if (!$databaseConfig['cleanup']) { return; } if (isset($this->databasesPopulated[$databaseKey]) && !$this->databasesPopulated[$databaseKey]) { return; } $dbh = $this->dbhs[$databaseKey]; if (!$dbh) { throw new ModuleConfigException( __CLASS__, 'No connection to database. Remove this module from config if you don\'t need database repopulation' ); } try { if (false === $this->shouldCleanup($databaseConfig, $databaseKey)) { return; } $this->drivers[$databaseKey]->cleanup(); $this->databasesPopulated[$databaseKey] = false; } catch (\Exception $e) { throw new ModuleException(__CLASS__, $e->getMessage()); } } /** * @param array $databaseConfig * @param string $databaseKey * @return bool */ protected function shouldCleanup($databaseConfig, $databaseKey) { // If using populator and it's not empty, clean up regardless if (!empty($databaseConfig['populator'])) { return true; } // If no sql dump for $databaseKey or sql dump is empty, don't clean up return !empty($this->databasesSql[$databaseKey]); } public function _isPopulated() { return $this->databasesPopulated[$this->currentDatabase]; } public function _loadDump($databaseKey = null, $databaseConfig = null) { $databaseKey = empty($databaseKey) ? self::DEFAULT_DATABASE : $databaseKey; $databaseConfig = empty($databaseConfig) ? $this->config : $databaseConfig; if ($databaseConfig['populator']) { $this->loadDumpUsingPopulator($databaseKey, $databaseConfig); return; } $this->loadDumpUsingDriver($databaseKey); } protected function loadDumpUsingPopulator($databaseKey, $databaseConfig) { $populator = new DbPopulator($databaseConfig); $this->databasesPopulated[$databaseKey] = $populator->run(); } protected function loadDumpUsingDriver($databaseKey) { if (!isset($this->databasesSql[$databaseKey])) { return; } if (!$this->databasesSql[$databaseKey]) { $this->debugSection('Db', 'No SQL loaded, loading dump skipped'); return; } $this->drivers[$databaseKey]->load($this->databasesSql[$databaseKey]); $this->databasesPopulated[$databaseKey] = true; } /** * Inserts an SQL record into a database. This record will be erased after the test. * * ```php * haveInDatabase('users', array('name' => 'miles', 'email' => 'miles@davis.com')); * ?> * ``` * * @param string $table * @param array $data * * @return integer $id */ public function haveInDatabase($table, array $data) { $lastInsertId = $this->_insertInDatabase($table, $data); $this->addInsertedRow($table, $data, $lastInsertId); return $lastInsertId; } public function _insertInDatabase($table, array $data) { $query = $this->_getDriver()->insert($table, $data); $parameters = array_values($data); $this->debugSection('Query', $query); $this->debugSection('Parameters', $parameters); $this->_getDriver()->executeQuery($query, $parameters); try { $lastInsertId = (int)$this->_getDriver()->lastInsertId($table); } catch (\PDOException $e) { // ignore errors due to uncommon DB structure, // such as tables without _id_seq in PGSQL $lastInsertId = 0; $this->debugSection('DB error', $e->getMessage()); } return $lastInsertId; } private function addInsertedRow($table, array $row, $id) { $primaryKey = $this->_getDriver()->getPrimaryKey($table); $primary = []; if ($primaryKey) { if ($id && count($primaryKey) === 1) { $primary [$primaryKey[0]] = $id; } else { foreach ($primaryKey as $column) { if (isset($row[$column])) { $primary[$column] = $row[$column]; } else { throw new \InvalidArgumentException( 'Primary key field ' . $column . ' is not set for table ' . $table ); } } } } else { $primary = $row; } $this->insertedRows[$this->currentDatabase][] = [ 'table' => $table, 'primary' => $primary, ]; } public function seeInDatabase($table, $criteria = []) { $res = $this->countInDatabase($table, $criteria); $this->assertGreaterThan( 0, $res, 'No matching records found for criteria ' . json_encode($criteria) . ' in table ' . $table ); } /** * Asserts that the given number of records were found in the database. * * ```php * seeNumRecords(1, 'users', ['name' => 'davert']) * ?> * ``` * * @param int $expectedNumber Expected number * @param string $table Table name * @param array $criteria Search criteria [Optional] */ public function seeNumRecords($expectedNumber, $table, array $criteria = []) { $actualNumber = $this->countInDatabase($table, $criteria); $this->assertEquals( $expectedNumber, $actualNumber, sprintf( 'The number of found rows (%d) does not match expected number %d for criteria %s in table %s', $actualNumber, $expectedNumber, json_encode($criteria), $table ) ); } public function dontSeeInDatabase($table, $criteria = []) { $count = $this->countInDatabase($table, $criteria); $this->assertLessThan( 1, $count, 'Unexpectedly found matching records for criteria ' . json_encode($criteria) . ' in table ' . $table ); } /** * Count rows in a database * * @param string $table Table name * @param array $criteria Search criteria [Optional] * * @return int */ protected function countInDatabase($table, array $criteria = []) { return (int) $this->proceedSeeInDatabase($table, 'count(*)', $criteria); } /** * Fetches all values from the column in database. * Provide table name, desired column and criteria. * * @param string $table * @param string $column * @param array $criteria * * @return mixed */ protected function proceedSeeInDatabase($table, $column, $criteria) { $query = $this->_getDriver()->select($column, $table, $criteria); $parameters = array_values($criteria); $this->debugSection('Query', $query); if (!empty($parameters)) { $this->debugSection('Parameters', $parameters); } $sth = $this->_getDriver()->executeQuery($query, $parameters); return $sth->fetchColumn(); } /** * Fetches all values from the column in database. * Provide table name, desired column and criteria. * * ``` php * grabColumnFromDatabase('users', 'email', array('name' => 'RebOOter')); * ``` * * @param string $table * @param string $column * @param array $criteria * * @return array */ public function grabColumnFromDatabase($table, $column, array $criteria = []) { $query = $this->_getDriver()->select($column, $table, $criteria); $parameters = array_values($criteria); $this->debugSection('Query', $query); $this->debugSection('Parameters', $parameters); $sth = $this->_getDriver()->executeQuery($query, $parameters); return $sth->fetchAll(\PDO::FETCH_COLUMN, 0); } /** * Fetches a single column value from a database. * Provide table name, desired column and criteria. * * ``` php * grabFromDatabase('users', 'email', array('name' => 'Davert')); * ``` * Comparison expressions can be used as well: * * ```php * grabFromDatabase('posts', ['num_comments >=' => 100]); * $user = $I->grabFromDatabase('users', ['email like' => 'miles%']); * ``` * * Supported operators: `<`, `>`, `>=`, `<=`, `!=`, `like`. * * @param string $table * @param string $column * @param array $criteria * * @return mixed Returns a single column value or false */ public function grabFromDatabase($table, $column, $criteria = []) { return $this->proceedSeeInDatabase($table, $column, $criteria); } /** * Returns the number of rows in a database * * @param string $table Table name * @param array $criteria Search criteria [Optional] * * @return int */ public function grabNumRecords($table, array $criteria = []) { return $this->countInDatabase($table, $criteria); } /** * Update an SQL record into a database. * * ```php * updateInDatabase('users', array('isAdmin' => true), array('email' => 'miles@davis.com')); * ?> * ``` * * @param string $table * @param array $data * @param array $criteria */ public function updateInDatabase($table, array $data, array $criteria = []) { $query = $this->_getDriver()->update($table, $data, $criteria); $parameters = array_merge(array_values($data), array_values($criteria)); $this->debugSection('Query', $query); if (!empty($parameters)) { $this->debugSection('Parameters', $parameters); } $this->_getDriver()->executeQuery($query, $parameters); } }