* @copyright Copyright (c) 2013 Arjay Angeles * @license MIT */ namespace Yajra\Pdo; use Exception; use OCICollection; use OCILob; use PDO; use PDOStatement; use Yajra\Pdo\Oci8\Exceptions\Oci8Exception; use Yajra\Pdo\Oci8\Statement; /** * Oci8 class to mimic the interface of the PDO class * This class extends PDO but overrides all of its methods. It does this so * that instanceof checks and type-hinting of existing code will work * seamlessly. */ class Oci8 extends PDO { /** * Database handler. * * @var resource */ private $dbh; /** * Driver options. * * @var array */ private array $options = []; /** * Whether currently in a transaction. * * @var bool */ private bool $inTransaction = false; /** * Insert query statement table variable. * * @var string */ private string $table; /** * Creates a PDO instance representing a connection to a database. * * Supports any connection string that is supported by oci_connect(), * or a valid PDO-style DSN (oci:host=host;port=port;dbname=dbname;charset=charset) * * @link https://www.php.net/manual/en/function.oci-connect.php * @link https://www.php.net/manual/en/pdo.construct.php * * @param string $dsn * @param string|null $username * @param string|null $password * @param array|null $options * * @throws Oci8Exception */ public function __construct(string $dsn, ?string $username, ?string $password, ?array $options = []) { $dsn = trim($dsn); if (str_starts_with($dsn, 'oci:')) { $connectStr = preg_replace('/^oci:/', '', $dsn); parse_str(str_replace(';', '&', $connectStr), $connectParams); if (empty($connectParams['dbname'])) { throw new Oci8Exception('Invalid connection string'); } else { $dsnStr = str_replace('//', '', $connectParams['dbname']); if (isset($connectParams['host']) && isset($connectParams['port'])) { $host = $connectParams['host'].':'.$connectParams['port']; } elseif (isset($connectParams['host'])) { $host = $connectParams['host']; } if (! empty($host)) { $dsnStr = $host.'/'.$dsnStr; } // A charset specified in the connection string takes // precedence over one specified in $options ! empty($connectParams['charset']) ? $charset = $this->configureCharset( $connectParams ) : $charset = $this->configureCharset($options); $dsn = $dsnStr; } } else { $charset = $this->configureCharset($options); } $this->connect($dsn, $username, $password, $options, $charset); // Save the options $this->options = $options; } /** * Configure proper charset. * * @param array $options * @return string */ private function configureCharset(array $options): string { $defaultCharset = 'AL32UTF8'; if (! empty($options['charset'])) { // Convert UTF8 charset to AL32UTF8 return strtolower($options['charset']) == 'utf8' ? $defaultCharset : $options['charset']; } return $defaultCharset; } /** * Connect to database. * * @param string $dsn * @param string $username * @param string $password * @param array $options * @param string $charset * * @throws Oci8Exception */ private function connect(string $dsn, string $username, string $password, array $options, string $charset) { $sessionMode = array_key_exists('session_mode', $options) ? $options['session_mode'] : OCI_DEFAULT; if (array_key_exists(PDO::ATTR_PERSISTENT, $options) && $options[PDO::ATTR_PERSISTENT]) { $this->dbh = oci_pconnect($username, $password, $dsn, $charset, $sessionMode); } else { $this->dbh = oci_connect($username, $password, $dsn, $charset, $sessionMode); } if (! $this->dbh) { $e = oci_error(); if (! str_contains($e['message'], 'the password will expire within')) { throw new Oci8Exception($e['message'], $e['code']); } } } /** * Return available drivers * Will insert the OCI driver on the list, if not exist. * * @return array with drivers */ public static function getAvailableDrivers(): array { $drivers = PDO::getAvailableDrivers(); if (! in_array('oci', $drivers)) { $drivers[] = 'oci'; } return $drivers; } /** * Initiates a transaction. * * @return bool TRUE on success or FALSE on failure * * @throws Oci8Exception */ public function beginTransaction(): bool { if ($this->inTransaction()) { throw new Oci8Exception('There is already an active transaction'); } $this->inTransaction = true; return true; } /** * Checks if inside a transaction. * * @return bool TRUE if a transaction is currently active, and FALSE if not. */ public function inTransaction(): bool { return $this->inTransaction; } /** * Returns true if the current process is in a transaction. * * @return bool * * @deprecated Use inTransaction() instead */ public function isTransaction(): bool { return $this->inTransaction(); } /** * Commits a transaction. * * @return bool TRUE on success or FALSE on failure. */ public function commit(): bool { if (oci_commit($this->dbh)) { $this->inTransaction = false; return true; } return false; } /** * Rolls back a transaction. * * @return bool TRUE on success or FALSE on failure. * * @throws Oci8Exception */ public function rollBack(): bool { if (! $this->inTransaction()) { throw new Oci8Exception('There is no active transaction'); } if (oci_rollback($this->dbh)) { $this->inTransaction = false; return true; } return false; } /** * Sets an attribute on the database handle. * * @param int $attribute * @param mixed $value * @return bool TRUE on success or FALSE on failure. */ public function setAttribute(int $attribute, mixed $value): bool { $this->options[$attribute] = $value; return true; } /** * Executes an SQL statement and returns the number of affected rows. * * @param string $statement The SQL statement to prepare and execute. * @return int The number of rows that were modified or deleted by the SQL * statement you issued. */ public function exec(string $statement): int { $stmt = $this->prepare($statement); $stmt->execute(); return $stmt->rowCount(); } /** * Prepares a statement for execution and returns a statement object. * * @param string $query This must be a valid SQL statement for the * target database server. * @param array|null $options [optional] This array holds one or more key=>value * pairs to set attribute values for the PDOStatement object that this * method returns. * @return Statement * * @throws Oci8Exception */ public function prepare(string $query, ?array $options = null): PDOStatement { // Get instance options if ($options == null) { $options = $this->options; } // Skip replacing ? with a pseudo named parameter on alter/create table command if ($this->isNamedParameterable($query)) { // Replace ? with a pseudo named parameter $parameter = 0; $query = preg_replace_callback('/(?:\'[^\']*\')(*SKIP)(*F)|\?/', function () use (&$parameter) { return ':p'.$parameter++; }, $query); } // check if statement is insert function if (str_contains(strtolower($query), 'insert into')) { preg_match('/insert into\s+([^\s\(]*)?/', strtolower($query), $matches); // store insert into table name $this->table = $matches[1]; } // Prepare the statement $sth = @oci_parse($this->dbh, $query); if (! $sth) { $e = oci_error($this->dbh); throw new Oci8Exception($e['message']); } if (! is_array($options)) { $options = []; } return new Statement($sth, $this, $options); } /** * Check if statement can use pseudo named parameter. * * @param string $statement * @return bool */ private function isNamedParameterable(string $statement): bool { return ! preg_match('/^alter+ +table/', strtolower(trim($statement))) and ! preg_match( '/^create+ +table/', strtolower(trim($statement)) ); } /** * returns the current value of the sequence related to the table where * record is inserted by default. The sequence name should follow this for it to work * properly: * {$table}.'_id_seq' * If the sequence name is passed, then the function will check using that value. * Oracle does not support the last inserted ID functionality like MySQL. * If the above sequence does not exist, the method will return 0;. * * @param string|null $name Sequence name * @return false|string Last sequence number or 0 if sequence does not exist * * @throws \ReflectionException */ public function lastInsertId(string $name = null): false|string { if (! isset($this->table)) { return 0; } if (is_null($name)) { $name = $this->table.'_id_seq'; } if (! $this->checkSequence($name)) { return 0; } $stmt = $this->query("SELECT $name.CURRVAL FROM DUAL", PDO::FETCH_COLUMN); return $stmt->fetch(); } /** * Special non PDO function to check if sequence exists. * * @param string $name * @return bool */ public function checkSequence(string $name): bool { try { $stmt = $this->query( "SELECT count(*) FROM ALL_SEQUENCES WHERE SEQUENCE_NAME=UPPER('$name') AND SEQUENCE_OWNER=UPPER(USER)", PDO::FETCH_COLUMN ); return $stmt->fetch(); } catch (Exception) { return false; } } /** * Executes an SQL statement, returning the results as a * Yajra\Pdo\Oci8\Statement object. * * @link https://php.net/manual/en/pdo.query.php * * @param string $statement

* The SQL statement to prepare and execute. *

*

* Data inside the query should be properly escaped. *

* @param int $mode The fetch mode must be one of the PDO::FETCH_* constants. * @param mixed $fetch_mode_args

* Arguments of custom class constructor when the mode * parameter is set to PDO::FETCH_CLASS. *

* @return Statement PDO::query returns a PDOStatement object, or FALSE * on failure. * * @see PDOStatement::setFetchMode For a full description of the second and following parameters. */ public function query($statement, $mode = null, ...$fetch_mode_args): PDOStatement { $stmt = $this->prepare($statement); $stmt->execute(); if ($mode) { $stmt->setFetchMode($mode, $fetch_mode_args); } return $stmt; } /** * Fetch the SQLSTATE associated with the last operation on the database * handle. * While this returns an error code, it merely emulates the action. If * there are no errors, it returns the success SQLSTATE code (00000). * If there are errors, it returns HY000. See errorInfo() to retrieve * the actual Oracle error code and message. * * @return string */ public function errorCode(): string { $error = $this->errorInfo(); return $error[0]; } /** * Returns extended error information for the last operation on the database handle. * The array consists of the following fields: * 0 SQLSTATE error code (a five characters alphanumeric identifier * defined in the ANSI SQL standard). * 1 Driver-specific error code. * 2 Driver-specific error message. * * @return array Error information */ public function errorInfo(): array { $e = $this->dbh ? oci_error($this->dbh) : null; if (is_array($e)) { return [ 'HY000', $e['code'], $e['message'], ]; } return ['00000', null, null]; } /** * Retrieve a database connection attribute. * * @param int $attribute * @return mixed A successful call returns the value of the requested PDO attribute. * An unsuccessful call returns null. */ public function getAttribute(int $attribute): mixed { if ($attribute == PDO::ATTR_DRIVER_NAME) { return 'oci8'; } if (isset($this->options[$attribute])) { return $this->options[$attribute]; } return null; } /** * Special non PDO function used to start cursors in the database * Remember to call oci_free_statement() on your cursor. * * @return resource|false New statement handle, or FALSE on error. */ public function getNewCursor(): mixed { return oci_new_cursor($this->dbh); } /** * Special non PDO function used to start descriptor in the database * Remember to call oci_free_statement() on your cursor. * * @param int $type One of OCI_DTYPE_FILE, OCI_DTYPE_LOB or OCI_DTYPE_ROWID. * @return bool|\OCILob New LOB or FILE descriptor on success, FALSE on error. */ public function getNewDescriptor(int $type = OCI_D_LOB): bool|OCILob { return oci_new_descriptor($this->dbh, $type); } /** * Special non PDO function used to close an open cursor in the database. * * @param resource $cursor A valid OCI statement identifier. * @return bool Returns TRUE on success or FALSE on failure. */ public function closeCursor($cursor): bool { return oci_free_statement($cursor); } /** * Places quotes around the input string * If you are using this function to build SQL statements, you are strongly * recommended to use prepare() to prepare SQL statements with bound * parameters instead of using quote() to interpolate user input into an SQL * statement. Prepared statements with bound parameters are not only more * portable, more convenient, immune to SQL injection, but are often much * faster to execute than interpolated queries, as both the server and * client side can cache a compiled form of the query. * * @param string $string The string to be quoted. * @param int $type Provides a data type hint for drivers that have * alternate quoting styles * @return string|false Returns a quoted string that is theoretically safe to pass * into an SQL statement. * * @todo Implement support for $paramType. */ public function quote(string $string, int $type = PDO::PARAM_STR): string|false { if (is_numeric($string)) { return $string; } return "'".str_replace("'", "''", $string)."'"; } /** * Sets a timeout limiting the maximum time a database round-trip. * * @param int $time_out * @return bool */ public function setCallTimeout(int $time_out): bool { if (! $this->dbh) { return false; } return oci_set_call_timeout($this->dbh, $time_out); } /** * Set the client identifier. * * @param $identifier * @return bool */ public function setClientIdentifier($identifier): bool { if (! $this->dbh) { return false; } return oci_set_client_identifier($this->dbh, $identifier); } /** * Special non PDO function * Allocates new collection object. * * @param string $typeName Should be a valid named type (uppercase). * @param string|null $schema Should point to the scheme, where the named type was created. * The name of the current user is the default value. * @return false|OCICollection */ public function getNewCollection(string $typeName, ?string $schema = null): false|OCICollection { return oci_new_collection($this->dbh, $typeName, $schema); } /** * Special not PDO function. * Get options used in creating the connection. * * @return array */ public function getOptions(): array { return $this->options; } /** * Special non PDO function to get DB resource. * * @return resource */ public function getResource() { return $this->dbh; } /** * Close the connection when object is removed. * * @link https://www.php.net/manual/en/pdo.connections.php PDO should remove the connection */ public function __destruct() { $this->close(); } /** * Close the connection. * * @link https://www.oracle.com/technetwork/topics/php/php-scalability-ha-twp-128842.pdf oci_close should be called * if the connection is pooled */ public function close() { if ($this->dbh) { oci_close($this->dbh); $this->dbh = null; } } }