disconnect(); } /** * Connects to the database if needed. * * @return void Returns void if the database connected successfully. * * @since 1.0 * @throws \RuntimeException */ public function connect() { if ($this->connection) { return; } // Make sure the PDO extension for PHP is installed and enabled. if (!static::isSupported()) { throw new UnsupportedAdapterException('PDO Extension is not available.', 1); } // Find the correct PDO DSN Format to use: switch ($this->options['driver']) { case 'cubrid': $this->options['port'] = $this->options['port'] ?? 33000; $format = 'cubrid:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; break; case 'dblib': $this->options['port'] = $this->options['port'] ?? 1433; $format = 'dblib:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; break; case 'firebird': $this->options['port'] = $this->options['port'] ?? 3050; $format = 'firebird:dbname=#DBNAME#'; $replace = ['#DBNAME#']; $with = [$this->options['database']]; break; case 'ibm': $this->options['port'] = $this->options['port'] ?? 56789; if (!empty($this->options['dsn'])) { $format = 'ibm:DSN=#DSN#'; $replace = ['#DSN#']; $with = [$this->options['dsn']]; } else { $format = 'ibm:hostname=#HOST#;port=#PORT#;database=#DBNAME#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; } break; case 'informix': $this->options['port'] = $this->options['port'] ?? 1526; $this->options['protocol'] = $this->options['protocol'] ?? 'onsoctcp'; if (!empty($this->options['dsn'])) { $format = 'informix:DSN=#DSN#'; $replace = ['#DSN#']; $with = [$this->options['dsn']]; } else { $format = 'informix:host=#HOST#;service=#PORT#;database=#DBNAME#;server=#SERVER#;protocol=#PROTOCOL#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#', '#SERVER#', '#PROTOCOL#']; $with = [ $this->options['host'], $this->options['port'], $this->options['database'], $this->options['server'], $this->options['protocol'], ]; } break; case 'mssql': $this->options['port'] = $this->options['port'] ?? 1433; $format = 'mssql:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; break; case 'mysql': $this->options['port'] = $this->options['port'] ?? 3306; if ($this->options['socket'] !== null) { $format = 'mysql:unix_socket=#SOCKET#;dbname=#DBNAME#;charset=#CHARSET#'; } else { $format = 'mysql:host=#HOST#;port=#PORT#;dbname=#DBNAME#;charset=#CHARSET#'; } $replace = ['#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#', '#CHARSET#']; $with = [ $this->options['host'], $this->options['port'], $this->options['socket'], $this->options['database'], $this->options['charset'] ]; break; case 'oci': $this->options['port'] = $this->options['port'] ?? 1521; $this->options['charset'] = $this->options['charset'] ?? 'AL32UTF8'; if (!empty($this->options['dsn'])) { $format = 'oci:dbname=#DSN#'; $replace = ['#DSN#']; $with = [$this->options['dsn']]; } else { $format = 'oci:dbname=//#HOST#:#PORT#/#DBNAME#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; } $format .= ';charset=' . $this->options['charset']; break; case 'odbc': $format = 'odbc:DSN=#DSN#;UID:#USER#;PWD=#PASSWORD#'; $replace = ['#DSN#', '#USER#', '#PASSWORD#']; $with = [$this->options['dsn'], $this->options['user'], $this->options['password']]; break; case 'pgsql': $this->options['port'] = $this->options['port'] ?? 5432; if ($this->options['socket'] !== null) { $format = 'pgsql:host=#SOCKET#;dbname=#DBNAME#'; } else { $format = 'pgsql:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; } $replace = ['#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['socket'], $this->options['database']]; // For data in transit TLS encryption. if ($this->options['ssl'] !== [] && $this->options['ssl']['enable'] === true) { if (isset($this->options['ssl']['verify_server_cert']) && $this->options['ssl']['verify_server_cert'] === true) { $format .= ';sslmode=verify-full'; } else { $format .= ';sslmode=require'; } $sslKeysMapping = [ 'cipher' => null, 'ca' => 'sslrootcert', 'capath' => null, 'key' => 'sslkey', 'cert' => 'sslcert', ]; // If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options. foreach ($sslKeysMapping as $key => $value) { if ($value !== null && $this->options['ssl'][$key] !== null) { $format .= ';' . $value . '=' . $this->options['ssl'][$key]; } } } break; case 'sqlite': if (isset($this->options['version']) && $this->options['version'] == 2) { $format = 'sqlite2:#DBNAME#'; } else { $format = 'sqlite:#DBNAME#'; } $replace = ['#DBNAME#']; $with = [$this->options['database']]; break; case 'sybase': $this->options['port'] = $this->options['port'] ?? 1433; $format = 'mssql:host=#HOST#;port=#PORT#;dbname=#DBNAME#'; $replace = ['#HOST#', '#PORT#', '#DBNAME#']; $with = [$this->options['host'], $this->options['port'], $this->options['database']]; break; default: throw new UnsupportedAdapterException('The ' . $this->options['driver'] . ' driver is not supported.'); } // Create the connection string: $connectionString = str_replace($replace, $with, $format); try { $this->connection = new \PDO( $connectionString, $this->options['user'], $this->options['password'], $this->options['driverOptions'] ); } catch (\PDOException $e) { throw new ConnectionFailureException('Could not connect to PDO: ' . $e->getMessage(), $e->getCode(), $e); } $this->setOption(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); $this->dispatchEvent(new ConnectionEvent(DatabaseEvents::POST_CONNECT, $this)); } /** * Method to escape a string for usage in an SQL statement. * * Oracle escaping reference: * http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F * * SQLite escaping notes: * http://www.sqlite.org/faq.html#q14 * * Method body is as implemented by the Zend Framework * * Note: Using query objects with bound variables is preferable to the below. * * @param string $text The string to be escaped. * @param boolean $extra Unused optional parameter to provide extra escaping. * * @return string The escaped string. * * @since 1.0 */ public function escape($text, $extra = false) { if (\is_int($text)) { return $text; } if (\is_float($text)) { // Force the dot as a decimal point. return str_replace(',', '.', (string) $text); } $text = str_replace("'", "''", (string) $text); return addcslashes($text, "\000\n\r\\\032"); } /** * Execute the SQL statement. * * @return boolean * * @since 1.0 * @throws \Exception * @throws \RuntimeException */ public function execute() { $this->connect(); // Take a local copy so that we don't modify the original query and cause issues later $sql = $this->replacePrefix((string) $this->sql); // Increment the query counter. $this->count++; // Get list of bounded parameters $bounded =& $this->sql->getBounded(); // If there is a monitor registered, let it know we are starting this query if ($this->monitor) { $this->monitor->startQuery($sql, $bounded); } // Execute the query. $this->executed = false; // Bind the variables foreach ($bounded as $key => $obj) { $this->statement->bindParam($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions); } try { $this->executed = $this->statement->execute(); // If there is a monitor registered, let it know we have finished this query if ($this->monitor) { $this->monitor->stopQuery(); } return true; } catch (\PDOException $exception) { // If there is a monitor registered, let it know we have finished this query if ($this->monitor) { $this->monitor->stopQuery(); } // Get the error number and message before we execute any more queries. $errorNum = (int) $this->statement->errorCode(); $errorMsg = (string) implode(', ', $this->statement->errorInfo()); // Check if the server was disconnected. try { if (!$this->connected()) { try { // Attempt to reconnect. $this->connection = null; $this->connect(); } catch (ConnectionFailureException $e) { // If connect fails, ignore that exception and throw the normal exception. throw new ExecutionFailureException($sql, $errorMsg, $errorNum); } // Since we were able to reconnect, run the query again. return $this->execute(); } } catch (\LogicException $e) { throw new ExecutionFailureException($sql, $errorMsg, $errorNum, $e); } // Throw the normal query exception. throw new ExecutionFailureException($sql, $errorMsg, $errorNum); } } /** * Retrieve a PDO database connection attribute * https://www.php.net/manual/en/pdo.getattribute.php * * Usage: $db->getOption(PDO::ATTR_CASE); * * @param mixed $key One of the PDO::ATTR_* Constants * * @return mixed * * @since 1.0 */ public function getOption($key) { $this->connect(); return $this->connection->getAttribute($key); } /** * Get the version of the database connector. * * @return string The database connector version. * * @since 1.5.0 */ public function getVersion() { $this->connect(); return $this->getOption(\PDO::ATTR_SERVER_VERSION); } /** * Get a query to run and verify the database is operational. * * @return string The query to check the health of the DB. * * @since 1.0 */ public function getConnectedQuery() { return 'SELECT 1'; } /** * Sets an attribute on the PDO database handle. * https://www.php.net/manual/en/pdo.setattribute.php * * Usage: $db->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER); * * @param integer $key One of the PDO::ATTR_* Constants * @param mixed $value One of the associated PDO Constants * related to the particular attribute * key. * * @return boolean * * @since 1.0 */ public function setOption($key, $value) { $this->connect(); return $this->connection->setAttribute($key, $value); } /** * Test to see if the PDO extension is available. * Override as needed to check for specific PDO Drivers. * * @return boolean True on success, false otherwise. * * @since 1.0 */ public static function isSupported() { return \defined('\\PDO::ATTR_DRIVER_NAME'); } /** * Determines if the connection to the server is active. * * @return boolean True if connected to the database engine. * * @since 1.0 * @throws \LogicException */ public function connected() { // Flag to prevent recursion into this function. static $checkingConnected = false; if ($checkingConnected) { // Reset this flag and throw an exception. $checkingConnected = false; throw new \LogicException('Recursion trying to check if connected.'); } // Backup the query state. $sql = $this->sql; $limit = $this->limit; $offset = $this->offset; $statement = $this->statement; try { // Set the checking connection flag. $checkingConnected = true; // Run a simple query to check the connection. $this->setQuery($this->getConnectedQuery()); $status = (bool) $this->loadResult(); } catch (\Exception $e) { // If we catch an exception here, we must not be connected. $status = false; } // Restore the query state. $this->sql = $sql; $this->limit = $limit; $this->offset = $offset; $this->statement = $statement; $checkingConnected = false; return $status; } /** * Method to get the auto-incremented value from the last INSERT statement. * * @return string The value of the auto-increment field from the last inserted row. * * @since 1.0 */ public function insertid() { $this->connect(); // Error suppress this to prevent PDO warning us that the driver doesn't support this operation. return @$this->connection->lastInsertId(); } /** * Select a database for use. * * @param string $database The name of the database to select for use. * * @return boolean True if the database was successfully selected. * * @since 1.0 * @throws \RuntimeException */ public function select($database) { $this->connect(); return true; } /** * Set the connection to use UTF-8 character encoding. * * @return boolean True on success. * * @since 1.0 */ public function setUtf() { return false; } /** * Method to commit a transaction. * * @param boolean $toSavepoint If true, commit to the last savepoint. * * @return void * * @since 1.0 * @throws \RuntimeException */ public function transactionCommit($toSavepoint = false) { $this->connect(); if (!$toSavepoint || $this->transactionDepth === 1) { $this->connection->commit(); } $this->transactionDepth--; } /** * Method to roll back a transaction. * * @param boolean $toSavepoint If true, rollback to the last savepoint. * * @return void * * @since 1.0 * @throws \RuntimeException */ public function transactionRollback($toSavepoint = false) { $this->connect(); if (!$toSavepoint || $this->transactionDepth === 1) { $this->connection->rollBack(); } $this->transactionDepth--; } /** * Method to initialize a transaction. * * @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created. * * @return void * * @since 1.0 * @throws \RuntimeException */ public function transactionStart($asSavepoint = false) { $this->connect(); if (!$asSavepoint || !$this->transactionDepth) { $this->connection->beginTransaction(); } $this->transactionDepth++; } /** * Prepares a SQL statement for execution * * @param string $query The SQL query to be prepared. * * @return StatementInterface * * @since 2.0.0 * @throws PrepareStatementFailureException */ protected function prepareStatement(string $query): StatementInterface { try { return new PdoStatement($this->connection->prepare($query, $this->options['driverOptions'])); } catch (\PDOException $exception) { throw new PrepareStatementFailureException($exception->getMessage(), $exception->getCode(), $exception); } } /** * PDO does not support serialize * * @return array * * @since 1.0 */ public function __sleep() { $serializedProperties = []; $reflect = new \ReflectionClass($this); // Get properties of the current class $properties = $reflect->getProperties(); foreach ($properties as $property) { // Do not serialize properties that are PDO if ($property->isStatic() === false && !($this->{$property->name} instanceof \PDO)) { $serializedProperties[] = $property->name; } } return $serializedProperties; } /** * Wake up after serialization * * @return void * * @since 1.0 */ public function __wakeup() { // Get connection back $this->__construct($this->options); } }