1<?php 2 3namespace Doctrine\DBAL\Driver\IBMDB2; 4 5use Doctrine\DBAL\Driver\Statement; 6use Doctrine\DBAL\Driver\StatementIterator; 7use Doctrine\DBAL\FetchMode; 8use Doctrine\DBAL\ParameterType; 9use IteratorAggregate; 10use PDO; 11use ReflectionClass; 12use ReflectionObject; 13use ReflectionProperty; 14use stdClass; 15 16use function array_change_key_case; 17use function assert; 18use function db2_bind_param; 19use function db2_execute; 20use function db2_fetch_array; 21use function db2_fetch_assoc; 22use function db2_fetch_both; 23use function db2_fetch_object; 24use function db2_free_result; 25use function db2_num_fields; 26use function db2_num_rows; 27use function db2_stmt_error; 28use function db2_stmt_errormsg; 29use function error_get_last; 30use function fclose; 31use function func_get_args; 32use function func_num_args; 33use function fwrite; 34use function gettype; 35use function is_int; 36use function is_object; 37use function is_resource; 38use function is_string; 39use function ksort; 40use function sprintf; 41use function stream_copy_to_stream; 42use function stream_get_meta_data; 43use function strtolower; 44use function tmpfile; 45 46use const CASE_LOWER; 47use const DB2_BINARY; 48use const DB2_CHAR; 49use const DB2_LONG; 50use const DB2_PARAM_FILE; 51use const DB2_PARAM_IN; 52 53class DB2Statement implements IteratorAggregate, Statement 54{ 55 /** @var resource */ 56 private $stmt; 57 58 /** @var mixed[] */ 59 private $bindParam = []; 60 61 /** 62 * Map of LOB parameter positions to the tuples containing reference to the variable bound to the driver statement 63 * and the temporary file handle bound to the underlying statement 64 * 65 * @var mixed[][] 66 */ 67 private $lobs = []; 68 69 /** @var string Name of the default class to instantiate when fetching class instances. */ 70 private $defaultFetchClass = '\stdClass'; 71 72 /** @var mixed[] Constructor arguments for the default class to instantiate when fetching class instances. */ 73 private $defaultFetchClassCtorArgs = []; 74 75 /** @var int */ 76 private $defaultFetchMode = FetchMode::MIXED; 77 78 /** 79 * Indicates whether the statement is in the state when fetching results is possible 80 * 81 * @var bool 82 */ 83 private $result = false; 84 85 /** 86 * @param resource $stmt 87 */ 88 public function __construct($stmt) 89 { 90 $this->stmt = $stmt; 91 } 92 93 /** 94 * {@inheritdoc} 95 */ 96 public function bindValue($param, $value, $type = ParameterType::STRING) 97 { 98 assert(is_int($param)); 99 100 return $this->bindParam($param, $value, $type); 101 } 102 103 /** 104 * {@inheritdoc} 105 */ 106 public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) 107 { 108 assert(is_int($param)); 109 110 switch ($type) { 111 case ParameterType::INTEGER: 112 $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG); 113 break; 114 115 case ParameterType::LARGE_OBJECT: 116 if (isset($this->lobs[$param])) { 117 [, $handle] = $this->lobs[$param]; 118 fclose($handle); 119 } 120 121 $handle = $this->createTemporaryFile(); 122 $path = stream_get_meta_data($handle)['uri']; 123 124 $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY); 125 126 $this->lobs[$param] = [&$variable, $handle]; 127 break; 128 129 default: 130 $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR); 131 break; 132 } 133 134 return true; 135 } 136 137 /** 138 * @param int $position Parameter position 139 * @param mixed $variable 140 * 141 * @throws DB2Exception 142 */ 143 private function bind($position, &$variable, int $parameterType, int $dataType): void 144 { 145 $this->bindParam[$position] =& $variable; 146 147 if (! db2_bind_param($this->stmt, $position, 'variable', $parameterType, $dataType)) { 148 throw new DB2Exception(db2_stmt_errormsg()); 149 } 150 } 151 152 /** 153 * {@inheritdoc} 154 */ 155 public function closeCursor() 156 { 157 $this->bindParam = []; 158 159 if (! db2_free_result($this->stmt)) { 160 return false; 161 } 162 163 $this->result = false; 164 165 return true; 166 } 167 168 /** 169 * {@inheritdoc} 170 */ 171 public function columnCount() 172 { 173 return db2_num_fields($this->stmt) ?: 0; 174 } 175 176 /** 177 * {@inheritdoc} 178 */ 179 public function errorCode() 180 { 181 return db2_stmt_error(); 182 } 183 184 /** 185 * {@inheritdoc} 186 */ 187 public function errorInfo() 188 { 189 return [ 190 db2_stmt_errormsg(), 191 db2_stmt_error(), 192 ]; 193 } 194 195 /** 196 * {@inheritdoc} 197 */ 198 public function execute($params = null) 199 { 200 if ($params === null) { 201 ksort($this->bindParam); 202 203 $params = []; 204 205 foreach ($this->bindParam as $column => $value) { 206 $params[] = $value; 207 } 208 } 209 210 foreach ($this->lobs as [$source, $target]) { 211 if (is_resource($source)) { 212 $this->copyStreamToStream($source, $target); 213 214 continue; 215 } 216 217 $this->writeStringToStream($source, $target); 218 } 219 220 $retval = db2_execute($this->stmt, $params); 221 222 foreach ($this->lobs as [, $handle]) { 223 fclose($handle); 224 } 225 226 $this->lobs = []; 227 228 if ($retval === false) { 229 throw new DB2Exception(db2_stmt_errormsg()); 230 } 231 232 $this->result = true; 233 234 return $retval; 235 } 236 237 /** 238 * {@inheritdoc} 239 */ 240 public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) 241 { 242 $this->defaultFetchMode = $fetchMode; 243 $this->defaultFetchClass = $arg2 ?: $this->defaultFetchClass; 244 $this->defaultFetchClassCtorArgs = $arg3 ? (array) $arg3 : $this->defaultFetchClassCtorArgs; 245 246 return true; 247 } 248 249 /** 250 * {@inheritdoc} 251 */ 252 public function getIterator() 253 { 254 return new StatementIterator($this); 255 } 256 257 /** 258 * {@inheritdoc} 259 */ 260 public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) 261 { 262 // do not try fetching from the statement if it's not expected to contain result 263 // in order to prevent exceptional situation 264 if (! $this->result) { 265 return false; 266 } 267 268 $fetchMode = $fetchMode ?: $this->defaultFetchMode; 269 switch ($fetchMode) { 270 case FetchMode::COLUMN: 271 return $this->fetchColumn(); 272 273 case FetchMode::MIXED: 274 return db2_fetch_both($this->stmt); 275 276 case FetchMode::ASSOCIATIVE: 277 return db2_fetch_assoc($this->stmt); 278 279 case FetchMode::CUSTOM_OBJECT: 280 $className = $this->defaultFetchClass; 281 $ctorArgs = $this->defaultFetchClassCtorArgs; 282 283 if (func_num_args() >= 2) { 284 $args = func_get_args(); 285 $className = $args[1]; 286 $ctorArgs = $args[2] ?? []; 287 } 288 289 $result = db2_fetch_object($this->stmt); 290 291 if ($result instanceof stdClass) { 292 $result = $this->castObject($result, $className, $ctorArgs); 293 } 294 295 return $result; 296 297 case FetchMode::NUMERIC: 298 return db2_fetch_array($this->stmt); 299 300 case FetchMode::STANDARD_OBJECT: 301 return db2_fetch_object($this->stmt); 302 303 default: 304 throw new DB2Exception('Given Fetch-Style ' . $fetchMode . ' is not supported.'); 305 } 306 } 307 308 /** 309 * {@inheritdoc} 310 */ 311 public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) 312 { 313 $rows = []; 314 315 switch ($fetchMode) { 316 case FetchMode::CUSTOM_OBJECT: 317 while (($row = $this->fetch(...func_get_args())) !== false) { 318 $rows[] = $row; 319 } 320 321 break; 322 323 case FetchMode::COLUMN: 324 while (($row = $this->fetchColumn()) !== false) { 325 $rows[] = $row; 326 } 327 328 break; 329 330 default: 331 while (($row = $this->fetch($fetchMode)) !== false) { 332 $rows[] = $row; 333 } 334 } 335 336 return $rows; 337 } 338 339 /** 340 * {@inheritdoc} 341 */ 342 public function fetchColumn($columnIndex = 0) 343 { 344 $row = $this->fetch(FetchMode::NUMERIC); 345 346 if ($row === false) { 347 return false; 348 } 349 350 return $row[$columnIndex] ?? null; 351 } 352 353 /** 354 * {@inheritdoc} 355 */ 356 public function rowCount() 357 { 358 return @db2_num_rows($this->stmt) ? : 0; 359 } 360 361 /** 362 * Casts a stdClass object to the given class name mapping its' properties. 363 * 364 * @param stdClass $sourceObject Object to cast from. 365 * @param string|object $destinationClass Name of the class or class instance to cast to. 366 * @param mixed[] $ctorArgs Arguments to use for constructing the destination class instance. 367 * 368 * @return object 369 * 370 * @throws DB2Exception 371 */ 372 private function castObject(stdClass $sourceObject, $destinationClass, array $ctorArgs = []) 373 { 374 if (! is_string($destinationClass)) { 375 if (! is_object($destinationClass)) { 376 throw new DB2Exception(sprintf( 377 'Destination class has to be of type string or object, %s given.', 378 gettype($destinationClass) 379 )); 380 } 381 } else { 382 $destinationClass = new ReflectionClass($destinationClass); 383 $destinationClass = $destinationClass->newInstanceArgs($ctorArgs); 384 } 385 386 $sourceReflection = new ReflectionObject($sourceObject); 387 $destinationClassReflection = new ReflectionObject($destinationClass); 388 /** @var ReflectionProperty[] $destinationProperties */ 389 $destinationProperties = array_change_key_case($destinationClassReflection->getProperties(), CASE_LOWER); 390 391 foreach ($sourceReflection->getProperties() as $sourceProperty) { 392 $sourceProperty->setAccessible(true); 393 394 $name = $sourceProperty->getName(); 395 $value = $sourceProperty->getValue($sourceObject); 396 397 // Try to find a case-matching property. 398 if ($destinationClassReflection->hasProperty($name)) { 399 $destinationProperty = $destinationClassReflection->getProperty($name); 400 401 $destinationProperty->setAccessible(true); 402 $destinationProperty->setValue($destinationClass, $value); 403 404 continue; 405 } 406 407 $name = strtolower($name); 408 409 // Try to find a property without matching case. 410 // Fallback for the driver returning either all uppercase or all lowercase column names. 411 if (isset($destinationProperties[$name])) { 412 $destinationProperty = $destinationProperties[$name]; 413 414 $destinationProperty->setAccessible(true); 415 $destinationProperty->setValue($destinationClass, $value); 416 417 continue; 418 } 419 420 $destinationClass->$name = $value; 421 } 422 423 return $destinationClass; 424 } 425 426 /** 427 * @return resource 428 * 429 * @throws DB2Exception 430 */ 431 private function createTemporaryFile() 432 { 433 $handle = @tmpfile(); 434 435 if ($handle === false) { 436 throw new DB2Exception('Could not create temporary file: ' . error_get_last()['message']); 437 } 438 439 return $handle; 440 } 441 442 /** 443 * @param resource $source 444 * @param resource $target 445 * 446 * @throws DB2Exception 447 */ 448 private function copyStreamToStream($source, $target): void 449 { 450 if (@stream_copy_to_stream($source, $target) === false) { 451 throw new DB2Exception('Could not copy source stream to temporary file: ' . error_get_last()['message']); 452 } 453 } 454 455 /** 456 * @param resource $target 457 * 458 * @throws DB2Exception 459 */ 460 private function writeStringToStream(string $string, $target): void 461 { 462 if (@fwrite($target, $string) === false) { 463 throw new DB2Exception('Could not write string to temporary file: ' . error_get_last()['message']); 464 } 465 } 466} 467