diff --git a/src/Driver/PDO/SQLSrv/Statement.php b/src/Driver/PDO/SQLSrv/Statement.php index cb2dfaedb78..0cca635516e 100644 --- a/src/Driver/PDO/SQLSrv/Statement.php +++ b/src/Driver/PDO/SQLSrv/Statement.php @@ -5,16 +5,26 @@ use Doctrine\DBAL\Driver\Exception\UnknownParameterType; use Doctrine\DBAL\Driver\Middleware\AbstractStatementMiddleware; use Doctrine\DBAL\Driver\PDO\Statement as PDOStatement; +use Doctrine\DBAL\Driver\Result; use Doctrine\DBAL\ParameterType; use Doctrine\Deprecations\Deprecation; use PDO; +use function fseek; +use function ftell; use function func_num_args; +use function is_resource; +use function stream_get_meta_data; + +use const SEEK_SET; final class Statement extends AbstractStatementMiddleware { private PDOStatement $statement; + /** @var mixed[]|null */ + private ?array $paramResources = null; + /** @internal The statement can be only instantiated by its driver connection. */ public function __construct(PDOStatement $statement) { @@ -104,6 +114,94 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool ); } + if ($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY) { + $this->trackParamResource($value); + } + return $this->bindParam($param, $value, $type); } + + /** + * {@inheritDoc} + */ + public function execute($params = null): Result + { + $resourceOffsets = $this->getResourceOffsets(); + try { + return parent::execute($params); + } finally { + if ($resourceOffsets !== null) { + $this->restoreResourceOffsets($resourceOffsets); + } + } + } + + /** + * Track a binary parameter reference at binding time. These + * are cached for later analysis by the getResourceOffsets. + * + * @param mixed $resource + */ + private function trackParamResource($resource): void + { + if (! is_resource($resource)) { + return; + } + + $this->paramResources ??= []; + $this->paramResources[] = $resource; + } + + /** + * Determine the offset that any resource parameters needs to be + * restored to after the statement is executed. Call immediately + * before execute (not during bindValue) to get the most accurate offset. + * + * @return int[]|null Return offsets to restore if needed. The array may be sparse. + */ + private function getResourceOffsets(): ?array + { + if ($this->paramResources === null) { + return null; + } + + $resourceOffsets = null; + foreach ($this->paramResources as $index => $resource) { + $position = false; + if (stream_get_meta_data($resource)['seekable']) { + $position = ftell($resource); + } + + if ($position === false) { + continue; + } + + $resourceOffsets ??= []; + $resourceOffsets[$index] = $position; + } + + if ($resourceOffsets === null) { + $this->paramResources = null; + } + + return $resourceOffsets; + } + + /** + * Restore resource offsets moved by PDOStatement->execute + * + * @param int[]|null $resourceOffsets The offsets returned by getResourceOffsets. + */ + private function restoreResourceOffsets(?array $resourceOffsets): void + { + if ($resourceOffsets === null || $this->paramResources === null) { + return; + } + + foreach ($resourceOffsets as $index => $offset) { + fseek($this->paramResources[$index], $offset, SEEK_SET); + } + + $this->paramResources = null; + } } diff --git a/src/Driver/SQLSrv/Statement.php b/src/Driver/SQLSrv/Statement.php index 227c33456c6..242ff1d870e 100644 --- a/src/Driver/SQLSrv/Statement.php +++ b/src/Driver/SQLSrv/Statement.php @@ -10,15 +10,20 @@ use Doctrine\Deprecations\Deprecation; use function assert; +use function fseek; +use function ftell; use function func_num_args; use function is_int; +use function is_resource; use function sqlsrv_execute; use function SQLSRV_PHPTYPE_STREAM; use function SQLSRV_PHPTYPE_STRING; use function sqlsrv_prepare; use function SQLSRV_SQLTYPE_VARBINARY; +use function stream_get_meta_data; use function stripos; +use const SEEK_SET; use const SQLSRV_ENC_BINARY; use const SQLSRV_ENC_CHAR; use const SQLSRV_PARAM_IN; @@ -58,6 +63,13 @@ final class Statement implements StatementInterface */ private array $types = []; + /** + * Resources used as bound values. + * + * @var mixed[]|null + */ + private ?array $paramResources = null; + /** * Append to any INSERT query to retrieve the last insert id. */ @@ -97,6 +109,10 @@ public function bindValue($param, $value, $type = ParameterType::STRING): bool ); } + if ($type === ParameterType::LARGE_OBJECT || $type === ParameterType::BINARY) { + $this->trackParamResource($value); + } + $this->variables[$param] = $value; $this->types[$param] = $type; @@ -159,10 +175,17 @@ public function execute($params = null): ResultInterface } } - $this->stmt ??= $this->prepare(); + $resourceOffsets = $this->getResourceOffsets(); + try { + $this->stmt ??= $this->prepare(); - if (! sqlsrv_execute($this->stmt)) { - throw Error::new(); + if (! sqlsrv_execute($this->stmt)) { + throw Error::new(); + } + } finally { + if ($resourceOffsets !== null) { + $this->restoreResourceOffsets($resourceOffsets); + } } return new Result($this->stmt); @@ -220,4 +243,73 @@ private function prepare() return $stmt; } + + /** + * Track a binary parameter reference at binding time. These + * are cached for later analysis by the getResourceOffsets. + * + * @param mixed $resource + */ + private function trackParamResource($resource): void + { + if (! is_resource($resource)) { + return; + } + + $this->paramResources ??= []; + $this->paramResources[] = $resource; + } + + /** + * Determine the offset that any resource parameters needs to be + * restored to after the statement is executed. Call immediately + * before execute (not during bindValue) to get the most accurate offset. + * + * @return int[]|null Return offsets to restore if needed. The array may be sparse. + */ + private function getResourceOffsets(): ?array + { + if ($this->paramResources === null) { + return null; + } + + $resourceOffsets = null; + foreach ($this->paramResources as $index => $resource) { + $position = false; + if (stream_get_meta_data($resource)['seekable']) { + $position = ftell($resource); + } + + if ($position === false) { + continue; + } + + $resourceOffsets ??= []; + $resourceOffsets[$index] = $position; + } + + if ($resourceOffsets === null) { + $this->paramResources = null; + } + + return $resourceOffsets; + } + + /** + * Restore resource offsets moved by PDOStatement->execute + * + * @param int[]|null $resourceOffsets The offsets returned by getResourceOffsets. + */ + private function restoreResourceOffsets(?array $resourceOffsets): void + { + if ($resourceOffsets === null || $this->paramResources === null) { + return; + } + + foreach ($resourceOffsets as $index => $offset) { + fseek($this->paramResources[$index], $offset, SEEK_SET); + } + + $this->paramResources = null; + } }