From cc0f01b794008692b2b82f658458841aee37367e Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 26 May 2023 09:41:42 +0200 Subject: [PATCH 1/6] chore: bump `doctrine/dbal` from version 2 to 3 --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index c279d4c..17d6455 100644 --- a/composer.json +++ b/composer.json @@ -13,15 +13,15 @@ ], "homepage": "https://github.com/ecphp/doctrine-oci8", "require": { - "php": ">= 7.4", + "php": ">= 8.0", "ext-oci8": "*", "ext-pdo": "*", - "doctrine/dbal": "^2.6" + "doctrine/dbal": "^3" }, "require-dev": { "ecphp/php-conventions": "^1", - "phpunit/phpunit": "^9", - "symfony/dotenv": "^5" + "phpunit/phpunit": "^10", + "symfony/dotenv": "^6" }, "autoload": { "psr-4": { From dbb3f7d7f56f7c0a83842a897c36170dea5ee5d1 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 20 Jun 2023 12:20:28 +0200 Subject: [PATCH 2/6] chore: update `.envrc` --- .envrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.envrc b/.envrc index aacea67..9d0a3a1 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,2 @@ use flake github:loophp/nix-auto-changelog -use flake github:loophp/nix-shell#env-php81-nts --impure +use flake github:loophp/nix-shell#env-php81 --impure From 8e4b123cc487ed20d27dcbd0b34bab705a4b0f67 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 20 Jun 2023 15:07:31 +0200 Subject: [PATCH 3/6] chore: update License --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6d0a0f9..3dce1b4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2019-2022, European Union. +Copyright (c) 2019-2023, European Union. Original work Copyright (c) 2018 Jason Hofer All rights reserved. From 71be97b1a39ff440676bb28752d5d9c96e49620b Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 20 Jun 2023 12:20:41 +0200 Subject: [PATCH 4/6] doc: add UML diagrams --- migration-doctrine-2.puml | 50 ++++++++++++++++++++++++++++++++ migration-doctrine-3.puml | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 migration-doctrine-2.puml create mode 100644 migration-doctrine-3.puml diff --git a/migration-doctrine-2.puml b/migration-doctrine-2.puml new file mode 100644 index 0000000..e72a207 --- /dev/null +++ b/migration-doctrine-2.puml @@ -0,0 +1,50 @@ +@startuml + +skin rose +hide empty members + +title ecphp/doctrine-oci8 migration to dbal3 + + +namespace doctrine.dbal { + abstract class AbstractDriverException + abstract class AbstractOracleDriver + + class OCI8Exception extends AbstractDriverException + + class Driver extends AbstractOracleDriver + + class OCI8Connection implements Connection, ServerInfoAwareConnection { + # dbh : resource + # executeMode : int + } + + class OCI8Statement implements IteratorAggregate, Statement { + # _dbh : resource + # _sth : resource + # _conn : OCI8Connection + # _PARAM : string + # fetchModeMap : int[] + # _defaultFetchMode : int + # _paramMap : string[] + # boundValues : mixed[] + # result : bool + } +} + +namespace ecphp.doctrine-oci8 { + class OCI8 + + class Driver extends doctrine.dbal.Driver + + class OCI8Connection extends doctrine.dbal.OCI8Connection { + + newCursor($sth = null): OCI8Cursor + + prepare($prepareString): OCI8Statement + } + + class OCI8Cursor extends doctrine.dbal.OCI8Statement + + class OCI8Statement extends doctrine.dbal.OCI8Statement +} + +@enduml diff --git a/migration-doctrine-3.puml b/migration-doctrine-3.puml new file mode 100644 index 0000000..86cd8a9 --- /dev/null +++ b/migration-doctrine-3.puml @@ -0,0 +1,61 @@ +@startuml + +skin rose +hide empty members + +title ecphp/doctrine-oci8 migration to dbal3 + +namespace doctrine.dbal { + together { + interface ServerInfoAwareConnection + interface Statement + interface IteratorAggregate + abstract class AbstractOracleDriver + class "Connection" as OCI8ConnectionDbal + class "OCI8Statement" as OCI8StatementDbal + class "Driver" as DriverDbal + } + + together { + OCI8ConnectionDbal -[dashed]-|> ServerInfoAwareConnection + class OCI8ConnectionDbal { + # dbh : resource + # executeMode : int + } + + OCI8StatementDbal -[dashed]-|> IteratorAggregate + OCI8StatementDbal -[dashed]-|> Statement + class OCI8StatementDbal { + # _dbh : resource + # _sth : resource + # _conn : OCI8Connection + # _PARAM : string + # fetchModeMap : int[] + # _defaultFetchMode : int + # _paramMap : string[] + # boundValues : mixed[] + # result : bool + } + + DriverDbal --|> AbstractOracleDriver + } +} + +namespace ecphp.doctrineoci8 { + together { + OCI8Cursor -[dashed]-|> doctrine.dbal.Statement + OCI8Cursor *-- doctrine.dbal.Statement + OCI8Cursor *-- OCI8Connection + + OCI8Connection -[dashed]-|> doctrine.dbal.ServerInfoAwareConnection + OCI8Connection *-- doctrine.dbal.OCI8ConnectionDbal + + Driver --|> doctrine.dbal.AbstractOracleDriver + Driver *-- doctrine.dbal.DriverDbal + + OCI8Statement -[dashed]-|> doctrine.dbal.Statement + OCI8Statement *-- doctrine.dbal.OCI8ConnectionDbal + } +} + +@enduml From 11126a9938b7434d36ddda1847b106f3043fc6c6 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 20 Jun 2023 12:21:03 +0200 Subject: [PATCH 5/6] refactor: switch to Doctrine 3 --- src/Doctrine/DBAL/Driver/OCI8/Connection.php | 118 ++++++ src/Doctrine/DBAL/Driver/OCI8/Cursor.php | 66 ++++ src/Doctrine/DBAL/Driver/OCI8/Driver.php | 35 +- .../DBAL/Driver/OCI8/OCI8Connection.php | 27 -- src/Doctrine/DBAL/Driver/OCI8/OCI8Cursor.php | 24 -- .../DBAL/Driver/OCI8/OCI8Statement.php | 344 ------------------ src/Doctrine/DBAL/Driver/OCI8/Result.php | 268 ++++++++++++++ src/Doctrine/DBAL/Driver/OCI8/Statement.php | 194 ++++++++++ 8 files changed, 660 insertions(+), 416 deletions(-) create mode 100644 src/Doctrine/DBAL/Driver/OCI8/Connection.php create mode 100644 src/Doctrine/DBAL/Driver/OCI8/Cursor.php delete mode 100644 src/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php delete mode 100644 src/Doctrine/DBAL/Driver/OCI8/OCI8Cursor.php delete mode 100644 src/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php create mode 100644 src/Doctrine/DBAL/Driver/OCI8/Result.php create mode 100644 src/Doctrine/DBAL/Driver/OCI8/Statement.php diff --git a/src/Doctrine/DBAL/Driver/OCI8/Connection.php b/src/Doctrine/DBAL/Driver/OCI8/Connection.php new file mode 100644 index 0000000..79ecf1b --- /dev/null +++ b/src/Doctrine/DBAL/Driver/OCI8/Connection.php @@ -0,0 +1,118 @@ +connection = $connection; + $this->parser = new Parser(false); + $this->executionMode = new ExecutionMode(); + } + + public function beginTransaction() + { + return $this->connection->beginTransaction(); + } + + public function commit() + { + return $this->connection->commit(); + } + + public function exec(string $sql): int + { + return $this->connection->exec($sql); + } + + public function getNativeConnection() + { + return $this->connection->getNativeConnection(); + } + + public function getServerVersion() + { + return $this->connection->getServerVersion(); + } + + public function lastInsertId($name = null) + { + return $this->connection->lastInsertId($name); + } + + public function newCursor($sth = null): Cursor + { + return new Cursor( + $this, + $sth, + ); + } + + public function prepare(string $sql): Statement + { + $visitor = new ConvertPositionalToNamedPlaceholders(); + + $this->parser->parse($sql, $visitor); + + $statement = oci_parse($this->connection->getNativeConnection(), $visitor->getSQL()); + assert(is_resource($statement)); + + $parameterMap = $visitor->getParameterMap(); + + return new Statement( + $this, + $statement, + $parameterMap, + $this->executionMode, + new DBALOCI8Statement( + $this->connection, + $statement, + $parameterMap, + $this->executionMode + ) + ); + } + + public function query(string $sql): Result + { + return $this->connection->query($sql); + } + + public function quote($value, $type = ParameterType::STRING) + { + return $this->connection->quote($value, $type); + } + + public function rollBack() + { + return $this->connection->rollBack(); + } +} diff --git a/src/Doctrine/DBAL/Driver/OCI8/Cursor.php b/src/Doctrine/DBAL/Driver/OCI8/Cursor.php new file mode 100644 index 0000000..b731353 --- /dev/null +++ b/src/Doctrine/DBAL/Driver/OCI8/Cursor.php @@ -0,0 +1,66 @@ +sth = $sth ?: oci_new_cursor($connection->getNativeConnection()); + + assert(is_resource($this->sth)); + + $this->decoratedStatement = new DriverOCI8Statement( + $this->connection, + $this->sth, + [], + new ExecutionMode() + ); + } + + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + return $this->decoratedStatement->bindParam($param, $variable, $type, $length); + } + + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + return $this->decoratedStatement->bindValue($param, $value, $type); + } + + public function execute($params = null): Result + { + return $this->decoratedStatement->execute($params); + } + + public function getStatementHandle() + { + return $this->sth; + } +} diff --git a/src/Doctrine/DBAL/Driver/OCI8/Driver.php b/src/Doctrine/DBAL/Driver/OCI8/Driver.php index 7df7322..0911bb4 100644 --- a/src/Doctrine/DBAL/Driver/OCI8/Driver.php +++ b/src/Doctrine/DBAL/Driver/OCI8/Driver.php @@ -11,34 +11,27 @@ namespace EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8; +use Doctrine\DBAL\Driver\AbstractOracleDriver; use Doctrine\DBAL\Driver\OCI8\Driver as BaseDriver; -use Doctrine\DBAL\Driver\OCI8\OCI8Exception; -use Doctrine\DBAL\Exception as DBALException; +use Doctrine\DBAL\Driver\OCI8\Exception\ConnectionFailed; use Throwable; -use const OCI_DEFAULT; - -final class Driver extends BaseDriver +final class Driver extends AbstractOracleDriver { + private BaseDriver $driver; + + public function __construct( + ) { + $this->driver = new BaseDriver(); + } + public function connect( array $params, - $username = null, - $password = null, - array $driverOptions = [] - ): OCI8Connection { + ): Connection { try { - $connection = new OCI8Connection( - $username, - $password, - $this->_constructDsn($params), - $params['charset'] ?? null, - $params['sessionMode'] ?? OCI_DEFAULT, - $params['persistent'] ?? false - ); - } catch (OCI8Exception $e) { - throw DBALException::driverException($this, $e); - } catch (Throwable $e) { - throw $e; + $connection = new Connection($this->driver->connect($params)); + } catch (Throwable) { + throw ConnectionFailed::new(); } return $connection; diff --git a/src/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php b/src/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php deleted file mode 100644 index dbdab46..0000000 --- a/src/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php +++ /dev/null @@ -1,27 +0,0 @@ -dbh, $this, $sth); - } - - public function prepare($prepareString): OCI8Statement - { - return new OCI8Statement($this->dbh, $prepareString, $this); - } -} diff --git a/src/Doctrine/DBAL/Driver/OCI8/OCI8Cursor.php b/src/Doctrine/DBAL/Driver/OCI8/OCI8Cursor.php deleted file mode 100644 index b4b4c9c..0000000 --- a/src/Doctrine/DBAL/Driver/OCI8/OCI8Cursor.php +++ /dev/null @@ -1,24 +0,0 @@ -_dbh = $dbh; - $this->_conn = $conn; - $this->_sth = $sth ?: oci_new_cursor($dbh); - } -} diff --git a/src/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php b/src/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php deleted file mode 100644 index 242dcab..0000000 --- a/src/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php +++ /dev/null @@ -1,344 +0,0 @@ -fetch(). - */ - private bool $returningCursors = false; - - /** - * Used because parent::fetchAll() calls $this->fetch(). - */ - private bool $returningResources = false; - - public function bindParam($column, &$variable, $type = ParameterType::STRING, $length = null): bool - { - $origCol = $column; - - $column = $this->_paramMap[$column] ?? $column; - - [$type, $ociType] = $this->normalizeType($type); - - // Type: Cursor. - if (PDO::PARAM_STMT === $type || OCI_B_CURSOR === $ociType) { - /** @var OCI8Connection $conn Because my IDE complains. */ - $conn = $this->_conn; - $variable = $conn->newCursor(); - - return $this->bindByName($column, $variable->_sth, -1, OCI_B_CURSOR); - } - - // Type: Null. (Must come *after* types that can expect $variable to be null, like 'cursor'.) - if (null === $variable) { - return $this->bindByName($column, $variable); - } - - // Type: Array. - if (is_array($variable)) { - $length = $length ?? -1; - - if (!$ociType) { - $ociType = PDO::PARAM_INT === $type ? SQLT_INT : SQLT_CHR; - } - - return $this->bindArrayByName( - $column, - $variable, - max(count($variable), 1), - empty($variable) ? 0 : $length, - $ociType - ); - } - - // Type: Lob - if (OCI_B_CLOB === $ociType || OCI_B_BLOB === $ociType) { - $type = PDO::PARAM_LOB; - } elseif ($ociType) { - return $this->bindByName($column, $variable, $length ?? -1, $ociType); - } - - return parent::bindParam($origCol, $variable, $type, $length); - } - - public function bindValue($param, $value, $type = ParameterType::STRING): bool - { - [$type, $ociType] = $this->normalizeType($type); - - if (PDO::PARAM_STMT === $type || OCI_B_CURSOR === $ociType) { - throw new LogicException('You must call "bindParam()" to bind a cursor.'); - } - - return parent::bindValue($param, $value, $type); - } - - public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) - { - [$fetchMode, $returnResources, $returnCursors] = $this->processFetchMode($fetchMode, true); - - // "Globals" are checked because parent::fetchAll() calls $this->fetch(). - $row = parent::fetch($fetchMode, $cursorOrientation, $cursorOffset); - - if (!$returnResources) { - $this->fetchCursorFields($row, $fetchMode, $returnCursors); - } - - return $row; - } - - /** - * @param null|mixed $fetchMode - * @param null|mixed $fetchArgument - * @param null|mixed $ctorArgs - * - * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception - */ - public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) - { - [$fetchMode, $this->returningResources, $this->returningCursors] = $this->processFetchMode($fetchMode); - - // "Globals" are set because parent::fetchAll() calls $this->fetch(). - $results = parent::fetchAll($fetchMode, $fetchArgument, $ctorArgs); - - if ( - !$this->returningResources - && $results - && is_array($results) - && OCI_BOTH !== self::$fetchModeMap[$fetchMode] // handled by $this->fetch() in parent::fetchAll(). - ) { - if (PDO::FETCH_COLUMN !== $fetchMode) { - foreach ($results as &$row) { - $this->fetchCursorFields($row, $fetchMode, $this->returningCursors); - } - unset($row); - } elseif (is_resource(reset($results))) { - $results = array_map(function ($value) use ($fetchMode) { - return $this->fetchCursorValue($value, $fetchMode, $this->returningCursors); - }, $results); - } - } - - $this->returningResources = - $this->returningCursors = false; - $this->resetCursorFields(); - - return $results; - } - - public function fetchColumn($columnIndex = 0, $fetchMode = null) - { - [$fetchMode, $returnResources, $returnCursors] = $this->processFetchMode($fetchMode); - - /** @var array|bool|resource|string|null $columnValue */ - $columnValue = parent::fetchColumn($columnIndex); - - if (!$returnResources && is_resource($columnValue)) { - return $this->fetchCursorValue($columnValue, $fetchMode, $returnCursors); - } - - return $columnValue; - } - - /** - * @param string $column - * @param mixed $variable - * @param int $maxTableLength - * @param int $maxItemLength - * @param int $type - */ - protected function bindArrayByName( - $column, - &$variable, - $maxTableLength, - $maxItemLength = -1, - $type = SQLT_AFC - ): bool { - // For PHP 7's OCI8 extension (prevents garbage collection). - $this->references[$column] = &$variable; - - return oci_bind_array_by_name($this->_sth, $column, $variable, $maxTableLength, $maxItemLength, $type); - } - - /** - * @param string $column - * @param mixed $variable - * @param int $maxLength - * @param int $type - */ - protected function bindByName($column, &$variable, $maxLength = -1, $type = SQLT_CHR): bool - { - // For PHP 7's OCI8 extension (prevents garbage collection). - $this->references[$column] = &$variable; - - return oci_bind_by_name($this->_sth, $column, $variable, $maxLength, $type); - } - - /** - * @param array|mixed $row - * @param int $fetchMode - * @param bool $returnCursors - * - * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception - */ - protected function fetchCursorFields(&$row, $fetchMode, $returnCursors): void - { - if (!is_array($row)) { - $this->resetCursorFields(); - } elseif (!$this->checkedForCursorFields) { - // This will also call fetchCursorField() on each cursor field of the first row. - $this->findCursorFields($row, $fetchMode, $returnCursors); - } elseif ($this->cursorFields) { - $shared = []; - - foreach ($this->cursorFields as $field) { - $key = (string) $row[$field]; - - if (isset($shared[$key])) { - $row[$field] = $shared[$key]; - - continue; - } - $row[$field] = $this->fetchCursorValue($row[$field], $fetchMode, $returnCursors); - $shared[$key] = &$row[$field]; - } - } - } - - /** - * @param resource $resource - * @param int $fetchMode - * @param bool $returnCursor - * - * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception - * - * @return array|mixed|OCI8Cursor - */ - protected function fetchCursorValue($resource, $fetchMode, $returnCursor) - { - /** @var OCI8Connection $conn Because my IDE complains. */ - $conn = $this->_conn; - $cursor = $conn->newCursor($resource); - - if ($returnCursor) { - return $cursor; - } - - $cursor->execute(); - $results = $cursor->fetchAll($fetchMode); - $cursor->closeCursor(); - - return $results; - } - - /** - * @param int $fetchMode - * @param bool $returnCursors - * - * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception - */ - protected function findCursorFields(array &$row, $fetchMode, $returnCursors): void - { - $shared = []; - - foreach ($row as $field => $value) { - if (!is_resource($value)) { - continue; - } - $this->cursorFields[] = $field; - $key = (string) $value; - - if (isset($shared[$key])) { - $row[$field] = $shared[$key]; - - continue; - } - // We are already here, so might as well process it. - $row[$field] = $this->fetchCursorValue($row[$field], $fetchMode, $returnCursors); - $shared[$key] = &$row[$field]; - } - $this->checkedForCursorFields = true; - } - - /** - * @param int|string $type - */ - protected function normalizeType($type): array - { - $ociType = null; - - // Figure out the type. - if (is_numeric($type)) { - $type = (int) $type; - - if (OCI8::isParamConstant($type)) { - $ociType = OCI8::decodeParamConstant($type); - } - } elseif ('cursor' === strtolower($type)) { - $type = PDO::PARAM_STMT; - $ociType = OCI_B_CURSOR; - } - - return [$type, $ociType]; - } - - /** - * @param int $fetchMode - * @param bool $checkGlobal - */ - protected function processFetchMode($fetchMode, $checkGlobal = false): array - { - $returnResources = ($checkGlobal && $this->returningResources) || ($fetchMode & OCI8::RETURN_RESOURCES); - $returnCursors = ($checkGlobal && $this->returningCursors) || ($fetchMode & OCI8::RETURN_CURSORS); - // Must unset the flags or there will be an error. - $fetchMode &= ~(OCI8::RETURN_RESOURCES + OCI8::RETURN_CURSORS); - $fetchMode = (int) ($fetchMode ?: $this->_defaultFetchMode); - - return [$fetchMode, $returnResources, $returnCursors]; - } - - protected function resetCursorFields(): void - { - $this->cursorFields = []; - $this->checkedForCursorFields = false; - } -} diff --git a/src/Doctrine/DBAL/Driver/OCI8/Result.php b/src/Doctrine/DBAL/Driver/OCI8/Result.php new file mode 100644 index 0000000..71cc474 --- /dev/null +++ b/src/Doctrine/DBAL/Driver/OCI8/Result.php @@ -0,0 +1,268 @@ +fetch(). + */ + private bool $returningCursors = false; + + /** + * Used because parent::fetchAll() calls $this->fetch(). + */ + private bool $returningResources = false; + + /** + * @var resource + */ + private $statement; + + /** + * @internal The result can be only instantiated by its driver connection or statement. + * + * @param resource $statement + * @param mixed $connection + */ + public function __construct($statement, $connection) + { + $this->statement = $statement; + $this->connection = $connection; + } + + public function columnCount(): int + { + $count = oci_num_fields($this->statement); + + if (false !== $count) { + return $count; + } + + return 0; + } + + public function fetchAllAssociative(): array + { + return $this->fetchAll(OCI_ASSOC, OCI_FETCHSTATEMENT_BY_ROW); + } + + public function fetchAllNumeric(): array + { + return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_ROW); + } + + public function fetchAssociative() + { + return $this->fetch(OCI_ASSOC); + } + + public function fetchFirstColumn(): array + { + return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_COLUMN)[0]; + } + + public function fetchNumeric() + { + return $this->fetch(OCI_NUM); + } + + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + public function free(): void + { + oci_cancel($this->statement); + } + + public function rowCount(): int + { + $count = oci_num_rows($this->statement); + + if (false !== $count) { + return $count; + } + + return 0; + } + + private function fetch(int $mode): array|false + { + [$fetchMode, $returnResources, $returnCursors] = $this->processFetchMode($mode, true); + + $row = oci_fetch_array($this->statement, $fetchMode | OCI_RETURN_NULLS | OCI_RETURN_LOBS); + + if (false === $row && oci_error($this->statement) !== false) { + throw Error::new($this->statement); + } + + if (!$returnResources) { + $this->fetchCursorFields($row, $fetchMode, $returnCursors); + } + + return $row; + } + + /** + * @return array + */ + private function fetchAll(int $mode, int $fetchStructure): array + { + oci_fetch_all( + $this->statement, + $result, + 0, + -1, + $mode | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS, + ); + + return $result; + } + + /** + * @param array|mixed $row + * @param int $fetchMode + * @param bool $returnCursors + * + * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception + */ + private function fetchCursorFields(&$row, $fetchMode, $returnCursors): void + { + if (!is_array($row)) { + $this->resetCursorFields(); + } elseif (!$this->checkedForCursorFields) { + // This will also call fetchCursorField() on each cursor field of the first row. + $this->findCursorFields($row, $fetchMode, $returnCursors); + } elseif ($this->cursorFields) { + $shared = []; + + foreach ($this->cursorFields as $field) { + $key = (string) $row[$field]; + + if (isset($shared[$key])) { + $row[$field] = $shared[$key]; + + continue; + } + $row[$field] = $this->fetchCursorValue($row[$field], $fetchMode, $returnCursors); + $shared[$key] = &$row[$field]; + } + } + } + + /** + * @param resource $resource + * @param int $fetchMode + * @param bool $returnCursor + * + * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception + * + * @return array|mixed|OCI8Cursor + */ + private function fetchCursorValue($resource, $fetchMode, $returnCursor) + { + /** @var OCI8Connection $conn Because my IDE complains. */ + $conn = $this->connection; + $cursor = $conn->newCursor($resource); + + if ($returnCursor) { + return $cursor; + } + + $cursor->execute(); + $results = $cursor->fetchAll($fetchMode); + $cursor->closeCursor(); + + return $results; + } + + /** + * @param int $fetchMode + * @param bool $returnCursors + * + * @throws \Doctrine\DBAL\Driver\OCI8\OCI8Exception + */ + private function findCursorFields(array &$row, $fetchMode, $returnCursors): void + { + $shared = []; + + foreach ($row as $field => $value) { + if (!is_resource($value)) { + continue; + } + $this->cursorFields[] = $field; + $key = (string) $value; + + if (isset($shared[$key])) { + $row[$field] = $shared[$key]; + + continue; + } + // We are already here, so might as well process it. + $row[$field] = $this->fetchCursorValue($row[$field], $fetchMode, $returnCursors); + $shared[$key] = &$row[$field]; + } + $this->checkedForCursorFields = true; + } + + /** + * @param int $fetchMode + * @param bool $checkGlobal + */ + private function processFetchMode($fetchMode, $checkGlobal = false): array + { + $returnResources = ($checkGlobal && $this->returningResources) || ($fetchMode & OCI8::RETURN_RESOURCES); + $returnCursors = ($checkGlobal && $this->returningCursors) || ($fetchMode & OCI8::RETURN_CURSORS); + // Must unset the flags or there will be an error. + $fetchMode &= ~(OCI8::RETURN_RESOURCES + OCI8::RETURN_CURSORS); + $fetchMode = (int) ($fetchMode ?: $this->_defaultFetchMode); + + return [$fetchMode, $returnResources, $returnCursors]; + } + + private function resetCursorFields(): void + { + $this->cursorFields = []; + $this->checkedForCursorFields = false; + } +} diff --git a/src/Doctrine/DBAL/Driver/OCI8/Statement.php b/src/Doctrine/DBAL/Driver/OCI8/Statement.php new file mode 100644 index 0000000..5cee322 --- /dev/null +++ b/src/Doctrine/DBAL/Driver/OCI8/Statement.php @@ -0,0 +1,194 @@ +parameterMap[$column] ?? $column; + + [$type, $ociType] = $this->normalizeType($type); + + // Type: Cursor. + if (PDO::PARAM_STMT === $type || OCI_B_CURSOR === $ociType) { + $variable = $this->connection->newCursor(); + $sth = $variable->getStatementHandle(); + + return $this->bindByName($column, $sth, -1, OCI_B_CURSOR); + } + + // Type: Null. (Must come *after* types that can expect $variable to be null, like 'cursor'.) + if (null === $variable) { + return $this->bindByName($column, $variable); + } + + // Type: Array. + if (is_array($variable)) { + $length = $length ?? -1; + + if (!$ociType) { + $ociType = PDO::PARAM_INT === $type ? SQLT_INT : SQLT_CHR; + } + + return $this->bindArrayByName( + $column, + $variable, + max(count($variable), 1), + empty($variable) ? 0 : $length, + $ociType + ); + } + + // Type: Lob + if (OCI_B_CLOB === $ociType || OCI_B_BLOB === $ociType) { + $type = PDO::PARAM_LOB; + } elseif ($ociType) { + return $this->bindByName($column, $variable, $length ?? -1, $ociType); + } + + return $this->decoratedStatement->bindParam($origCol, $variable, $type, $length); + } + + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + [$type, $ociType] = $this->normalizeType($type); + + if (PDO::PARAM_STMT === $type || OCI_B_CURSOR === $ociType) { + throw new LogicException('You must call "bindParam()" to bind a cursor.'); + } + + return $this->decoratedStatement->bindValue($param, $value, $type); + } + + public function execute($params = null): Result + { + if (null !== $params) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + + foreach ($params as $key => $val) { + if (is_int($key)) { + $this->bindValue($key + 1, $val, ParameterType::STRING); + } else { + $this->bindValue($key, $val, ParameterType::STRING); + } + } + } + + if ($this->executionMode->isAutoCommitEnabled()) { + $mode = OCI_COMMIT_ON_SUCCESS; + } else { + $mode = OCI_NO_AUTO_COMMIT; + } + + $ret = oci_execute($this->statement, $mode); + + if (!$ret) { + throw Error::new($this->statement); + } + + return new Result($this->statement, $this->connection); + } + + /** + * @param string $column + * @param mixed $variable + * @param int $maxTableLength + * @param int $maxItemLength + * @param int $type + */ + private function bindArrayByName( + $column, + &$variable, + $maxTableLength, + $maxItemLength = -1, + $type = SQLT_AFC + ): bool { + // For PHP 7's OCI8 extension (prevents garbage collection). + $this->references[$column] = &$variable; + + return oci_bind_array_by_name($this->statement, $column, $variable, $maxTableLength, $maxItemLength, $type); + } + + /** + * @param string $column + * @param mixed $variable + * @param int $maxLength + * @param int $type + */ + private function bindByName($column, &$variable, $maxLength = -1, $type = SQLT_CHR): bool + { + // For PHP 7's OCI8 extension (prevents garbage collection). + $this->references[$column] = &$variable; + + return oci_bind_by_name($this->statement, $column, $variable, $maxLength, $type); + } + + /** + * @return array{0: array-key, int|null} + */ + private function normalizeType(int|string $type): array + { + return match (true) { + is_numeric($type) => [(int) $type, OCI8::isParamConstant((int) $type) ? OCI8::decodeParamConstant((int) $type) : null], + 'cursor' === strtolower($type) => [PDO::PARAM_STMT, OCI_B_CURSOR], + default => [$type, null], + }; + } +} From b38a198b4be83ae8421883fdb710a40bfeeb359c Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Tue, 20 Jun 2023 15:07:49 +0200 Subject: [PATCH 6/6] tests: update tests accordingly --- phpunit.xml | 23 ++++++++++++++----- tests/AbstractTestCase.php | 12 ++++------ .../Doctrine/DBAL/Driver/OCI8/DriverTest.php | 3 ++- .../DBAL/Driver/OCI8/OCI8ConnectionTest.php | 3 ++- .../DBAL/Driver/OCI8/OCI8StatementTest.php | 21 +++++++++-------- tests/Doctrine/DBAL/Driver/OCI8/OCI8Test.php | 1 + tests/OciWrapper.php | 16 +++++++------ tests/bootstrap.php | 3 +-- 8 files changed, 49 insertions(+), 33 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index b850390..c107534 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,13 +1,24 @@ - - - - src - - + + tests + + + src + + diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index d208aff..26e2138 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -18,8 +18,6 @@ use PHPUnit\Framework\TestCase; use ReflectionObject; -use function getenv; - /** * @internal */ @@ -42,11 +40,11 @@ protected function getConnection(): DBAL\Connection } $params = [ - 'user' => getenv('DB_USER'), - 'password' => getenv('DB_PASSWORD'), - 'host' => getenv('DB_HOST'), - 'port' => getenv('DB_PORT'), - 'dbname' => getenv('DB_SCHEMA'), + 'user' => $_ENV['DB_USER'], + 'password' => $_ENV['DB_PASSWORD'], + 'host' => $_ENV['DB_HOST'], + 'port' => $_ENV['DB_PORT'], + 'dbname' => $_ENV['DB_SCHEMA'], 'driverClass' => Driver::class, ]; diff --git a/tests/Doctrine/DBAL/Driver/OCI8/DriverTest.php b/tests/Doctrine/DBAL/Driver/OCI8/DriverTest.php index bbc6936..c70d6c2 100644 --- a/tests/Doctrine/DBAL/Driver/OCI8/DriverTest.php +++ b/tests/Doctrine/DBAL/Driver/OCI8/DriverTest.php @@ -12,11 +12,12 @@ namespace tests\EcPhp\DoctrineOci8\Doctrine\DBAL\Test\Driver\OCI8; use Doctrine\DBAL\Types\Type; -use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\OCI8Connection; +use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\Connection as OCI8Connection; use tests\EcPhp\DoctrineOci8\AbstractTestCase; /** * @internal + * * @coversNothing */ final class DriverTest extends AbstractTestCase diff --git a/tests/Doctrine/DBAL/Driver/OCI8/OCI8ConnectionTest.php b/tests/Doctrine/DBAL/Driver/OCI8/OCI8ConnectionTest.php index 24ae1ff..7268dd8 100644 --- a/tests/Doctrine/DBAL/Driver/OCI8/OCI8ConnectionTest.php +++ b/tests/Doctrine/DBAL/Driver/OCI8/OCI8ConnectionTest.php @@ -11,11 +11,12 @@ namespace tests\EcPhp\DoctrineOci8\Doctrine\DBAL\Test\Driver\OCI8; -use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\OCI8Statement; +use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\Statement as OCI8Statement; use tests\EcPhp\DoctrineOci8\AbstractTestCase; /** * @internal + * * @coversNothing */ final class OCI8ConnectionTest extends AbstractTestCase diff --git a/tests/Doctrine/DBAL/Driver/OCI8/OCI8StatementTest.php b/tests/Doctrine/DBAL/Driver/OCI8/OCI8StatementTest.php index bcea07e..3a26bdc 100644 --- a/tests/Doctrine/DBAL/Driver/OCI8/OCI8StatementTest.php +++ b/tests/Doctrine/DBAL/Driver/OCI8/OCI8StatementTest.php @@ -11,8 +11,9 @@ namespace tests\EcPhp\DoctrineOci8\Doctrine\DBAL\Test\Driver\OCI8; +use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\Cursor; use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\OCI8; -use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\OCI8Cursor; +use EcPhp\DoctrineOci8\Doctrine\DBAL\Driver\OCI8\Statement; use LogicException; use PDO; use tests\EcPhp\DoctrineOci8\AbstractTestCase; @@ -22,6 +23,7 @@ /** * @internal + * * @coversNothing */ final class OCI8StatementTest extends AbstractTestCase @@ -68,9 +70,9 @@ public function testBindParamSetsOci8Cursor(): void $stmt->bindParam('cursor2', $cursor2, OCI8::PARAM_CURSOR); $stmt->bindParam('cursor3', $cursor3, PDO::PARAM_STMT); - self::assertInstanceOf(OCI8Cursor::class, $cursor1); - self::assertInstanceOf(OCI8Cursor::class, $cursor2); - self::assertInstanceOf(OCI8Cursor::class, $cursor3); + self::assertInstanceOf(Cursor::class, $cursor1); + self::assertInstanceOf(Cursor::class, $cursor2); + self::assertInstanceOf(Cursor::class, $cursor3); } public function testBindValueThrowsExceptionWhenTypeIsCursor(): void @@ -109,14 +111,15 @@ public function testBindValueThrowsExceptionWhenTypeIsPdoStmt(): void public function testCursorFetchAll(): void { $conn = $this->getConnection(); + /** @var Statement */ $stmt = $conn->prepare('BEGIN FIRST_NAMES(:cursor); END;'); /** @var OCI8Cursor $cursor */ $stmt->bindParam('cursor', $cursor, 'cursor'); $stmt->execute(); - $cursor->execute(); + $result = $cursor->execute(); - $results = $cursor->fetchAll(PDO::FETCH_ASSOC); + $results = $result->fetchAllAssociative(); self::assertSame(self::$employees, $results); } @@ -126,14 +129,14 @@ public function testCursorFetchColumn(): void $conn = $this->getConnection(); $stmt = $conn->prepare('BEGIN FIRST_NAMES(:cursor); END;'); - /** @var OCI8Cursor $cursor */ + /** @var Cursor $cursor */ $stmt->bindParam('cursor', $cursor, 'cursor'); $stmt->execute(); - $cursor->execute(); + $result = $cursor->execute(); $results = []; - while (false !== ($columnValue = $cursor->fetchColumn())) { + while (false !== ($columnValue = $result->fetchOne())) { $results[] = $columnValue; } diff --git a/tests/Doctrine/DBAL/Driver/OCI8/OCI8Test.php b/tests/Doctrine/DBAL/Driver/OCI8/OCI8Test.php index f3817cd..3b626c6 100644 --- a/tests/Doctrine/DBAL/Driver/OCI8/OCI8Test.php +++ b/tests/Doctrine/DBAL/Driver/OCI8/OCI8Test.php @@ -20,6 +20,7 @@ /** * @internal + * * @coversNothing */ final class OCI8Test extends AbstractTestCase diff --git a/tests/OciWrapper.php b/tests/OciWrapper.php index 7a9fd1f..e8a0405 100644 --- a/tests/OciWrapper.php +++ b/tests/OciWrapper.php @@ -11,6 +11,8 @@ namespace tests\EcPhp\DoctrineOci8; +use RuntimeException; + use const OCI_DEFAULT; final class OciWrapper @@ -25,9 +27,9 @@ public function __construct() // We must change the password in order to take this in account. oci_password_change( $this->connect(), - getenv('DB_USER'), - getenv('DB_PASSWORD'), - getenv('DB_PASSWORD') + $_ENV['DB_USER'], + $_ENV['DB_PASSWORD'], + $_ENV['DB_PASSWORD'] ); } @@ -43,10 +45,10 @@ public function connect() { if (!$this->dbh) { $this->dbh = oci_connect( - getenv('DB_USER'), - getenv('DB_PASSWORD'), - '//' . getenv('DB_HOST') . ':' . getenv('DB_PORT') . '/' . getenv('DB_SCHEMA'), - getenv('DB_CHARSET'), + $_ENV['DB_USER'], + $_ENV['DB_PASSWORD'], + '//' . $_ENV['DB_HOST'] . ':' . $_ENV['DB_PORT'] . '/' . $_ENV['DB_SCHEMA'], + $_ENV['DB_CHARSET'], OCI_DEFAULT ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 2b2f3c4..50862c5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,5 +13,4 @@ $autoloader = require __DIR__ . '/../vendor/autoload.php'; -$dotenv = new Dotenv(true); -$dotenv->loadEnv(__DIR__ . '/../.env'); +(new Dotenv())->loadEnv(__DIR__ . '/../.env');