1<?php 2 3namespace Doctrine\DBAL\Driver\Mysqli; 4 5use Doctrine\DBAL\Driver\FetchUtils; 6use Doctrine\DBAL\Driver\Mysqli\Exception\ConnectionError; 7use Doctrine\DBAL\Driver\Mysqli\Exception\FailedReadingStreamOffset; 8use Doctrine\DBAL\Driver\Mysqli\Exception\StatementError; 9use Doctrine\DBAL\Driver\Mysqli\Exception\UnknownType; 10use Doctrine\DBAL\Driver\Result; 11use Doctrine\DBAL\Driver\Statement as StatementInterface; 12use Doctrine\DBAL\Driver\StatementIterator; 13use Doctrine\DBAL\Exception\InvalidArgumentException; 14use Doctrine\DBAL\FetchMode; 15use Doctrine\DBAL\ParameterType; 16use IteratorAggregate; 17use mysqli; 18use mysqli_sql_exception; 19use mysqli_stmt; 20use PDO; 21use ReturnTypeWillChange; 22 23use function array_combine; 24use function array_fill; 25use function assert; 26use function count; 27use function feof; 28use function fread; 29use function get_resource_type; 30use function is_array; 31use function is_int; 32use function is_resource; 33use function sprintf; 34use function str_repeat; 35 36/** 37 * @deprecated Use {@link Statement} instead 38 */ 39class MysqliStatement implements IteratorAggregate, StatementInterface, Result 40{ 41 /** @var string[] */ 42 protected static $_paramTypeMap = [ 43 ParameterType::ASCII => 's', 44 ParameterType::STRING => 's', 45 ParameterType::BINARY => 's', 46 ParameterType::BOOLEAN => 'i', 47 ParameterType::NULL => 's', 48 ParameterType::INTEGER => 'i', 49 ParameterType::LARGE_OBJECT => 'b', 50 ]; 51 52 /** @var mysqli */ 53 protected $_conn; 54 55 /** @var mysqli_stmt */ 56 protected $_stmt; 57 58 /** @var string[]|false|null */ 59 protected $_columnNames; 60 61 /** @var mixed[] */ 62 protected $_rowBindedValues = []; 63 64 /** @var mixed[] */ 65 protected $_bindedValues; 66 67 /** @var string */ 68 protected $types; 69 70 /** 71 * Contains ref values for bindValue(). 72 * 73 * @var mixed[] 74 */ 75 protected $_values = []; 76 77 /** @var int */ 78 protected $_defaultFetchMode = FetchMode::MIXED; 79 80 /** 81 * Indicates whether the statement is in the state when fetching results is possible 82 * 83 * @var bool 84 */ 85 private $result = false; 86 87 /** 88 * @internal The statement can be only instantiated by its driver connection. 89 * 90 * @param string $prepareString 91 * 92 * @throws MysqliException 93 */ 94 public function __construct(mysqli $conn, $prepareString) 95 { 96 $this->_conn = $conn; 97 98 try { 99 $stmt = $conn->prepare($prepareString); 100 } catch (mysqli_sql_exception $e) { 101 throw ConnectionError::upcast($e); 102 } 103 104 if ($stmt === false) { 105 throw ConnectionError::new($this->_conn); 106 } 107 108 $this->_stmt = $stmt; 109 110 $paramCount = $this->_stmt->param_count; 111 $this->types = str_repeat('s', $paramCount); 112 $this->_bindedValues = array_fill(1, $paramCount, null); 113 } 114 115 /** 116 * {@inheritdoc} 117 */ 118 public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) 119 { 120 assert(is_int($param)); 121 122 if (! isset(self::$_paramTypeMap[$type])) { 123 throw UnknownType::new($type); 124 } 125 126 $this->_bindedValues[$param] =& $variable; 127 $this->types[$param - 1] = self::$_paramTypeMap[$type]; 128 129 return true; 130 } 131 132 /** 133 * {@inheritdoc} 134 */ 135 public function bindValue($param, $value, $type = ParameterType::STRING) 136 { 137 assert(is_int($param)); 138 139 if (! isset(self::$_paramTypeMap[$type])) { 140 throw UnknownType::new($type); 141 } 142 143 $this->_values[$param] = $value; 144 $this->_bindedValues[$param] =& $this->_values[$param]; 145 $this->types[$param - 1] = self::$_paramTypeMap[$type]; 146 147 return true; 148 } 149 150 /** 151 * {@inheritdoc} 152 */ 153 public function execute($params = null) 154 { 155 if ($params !== null && count($params) > 0) { 156 if (! $this->bindUntypedValues($params)) { 157 throw StatementError::new($this->_stmt); 158 } 159 } elseif (count($this->_bindedValues) > 0) { 160 $this->bindTypedParameters(); 161 } 162 163 try { 164 $result = $this->_stmt->execute(); 165 } catch (mysqli_sql_exception $e) { 166 throw StatementError::upcast($e); 167 } 168 169 if (! $result) { 170 throw StatementError::new($this->_stmt); 171 } 172 173 if ($this->_columnNames === null) { 174 $meta = $this->_stmt->result_metadata(); 175 if ($meta !== false) { 176 $fields = $meta->fetch_fields(); 177 assert(is_array($fields)); 178 179 $columnNames = []; 180 foreach ($fields as $col) { 181 $columnNames[] = $col->name; 182 } 183 184 $meta->free(); 185 186 $this->_columnNames = $columnNames; 187 } else { 188 $this->_columnNames = false; 189 } 190 } 191 192 if ($this->_columnNames !== false) { 193 // Store result of every execution which has it. Otherwise it will be impossible 194 // to execute a new statement in case if the previous one has non-fetched rows 195 // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html 196 $this->_stmt->store_result(); 197 198 // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql, 199 // it will have to allocate as much memory as it may be needed for the given column type 200 // (e.g. for a LONGBLOB column it's 4 gigabytes) 201 // @link https://bugs.php.net/bug.php?id=51386#1270673122 202 // 203 // Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been 204 // previously called on the statement, the values are unbound making the statement unusable. 205 // 206 // It's also important that row values are bound after _each_ call to store_result(). Otherwise, 207 // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated 208 // to the length of the ones fetched during the previous execution. 209 $this->_rowBindedValues = array_fill(0, count($this->_columnNames), null); 210 211 $refs = []; 212 foreach ($this->_rowBindedValues as $key => &$value) { 213 $refs[$key] =& $value; 214 } 215 216 if (! $this->_stmt->bind_result(...$refs)) { 217 throw StatementError::new($this->_stmt); 218 } 219 } 220 221 $this->result = true; 222 223 return true; 224 } 225 226 /** 227 * Binds parameters with known types previously bound to the statement 228 */ 229 private function bindTypedParameters(): void 230 { 231 $streams = $values = []; 232 $types = $this->types; 233 234 foreach ($this->_bindedValues as $parameter => $value) { 235 assert(is_int($parameter)); 236 237 if (! isset($types[$parameter - 1])) { 238 $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING]; 239 } 240 241 if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) { 242 if (is_resource($value)) { 243 if (get_resource_type($value) !== 'stream') { 244 throw new InvalidArgumentException( 245 'Resources passed with the LARGE_OBJECT parameter type must be stream resources.' 246 ); 247 } 248 249 $streams[$parameter] = $value; 250 $values[$parameter] = null; 251 continue; 252 } 253 254 $types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING]; 255 } 256 257 $values[$parameter] = $value; 258 } 259 260 if (! $this->_stmt->bind_param($types, ...$values)) { 261 throw StatementError::new($this->_stmt); 262 } 263 264 $this->sendLongData($streams); 265 } 266 267 /** 268 * Handle $this->_longData after regular query parameters have been bound 269 * 270 * @param array<int, resource> $streams 271 * 272 * @throws MysqliException 273 */ 274 private function sendLongData(array $streams): void 275 { 276 foreach ($streams as $paramNr => $stream) { 277 while (! feof($stream)) { 278 $chunk = fread($stream, 8192); 279 280 if ($chunk === false) { 281 throw FailedReadingStreamOffset::new($paramNr); 282 } 283 284 if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) { 285 throw StatementError::new($this->_stmt); 286 } 287 } 288 } 289 } 290 291 /** 292 * Binds a array of values to bound parameters. 293 * 294 * @param mixed[] $values 295 * 296 * @return bool 297 */ 298 private function bindUntypedValues(array $values) 299 { 300 $params = []; 301 $types = str_repeat('s', count($values)); 302 303 foreach ($values as &$v) { 304 $params[] =& $v; 305 } 306 307 return $this->_stmt->bind_param($types, ...$params); 308 } 309 310 /** 311 * @return mixed[]|false|null 312 * 313 * @throws StatementError 314 */ 315 private function _fetch() 316 { 317 try { 318 $ret = $this->_stmt->fetch(); 319 } catch (mysqli_sql_exception $e) { 320 throw StatementError::upcast($e); 321 } 322 323 if ($ret === true) { 324 $values = []; 325 foreach ($this->_rowBindedValues as $v) { 326 $values[] = $v; 327 } 328 329 return $values; 330 } 331 332 return $ret; 333 } 334 335 /** 336 * {@inheritdoc} 337 * 338 * @deprecated Use fetchNumeric(), fetchAssociative() or fetchOne() instead. 339 */ 340 public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) 341 { 342 // do not try fetching from the statement if it's not expected to contain result 343 // in order to prevent exceptional situation 344 if (! $this->result) { 345 return false; 346 } 347 348 $fetchMode = $fetchMode ?: $this->_defaultFetchMode; 349 350 if ($fetchMode === FetchMode::COLUMN) { 351 return $this->fetchColumn(); 352 } 353 354 $values = $this->_fetch(); 355 356 if ($values === null) { 357 return false; 358 } 359 360 if ($values === false) { 361 throw StatementError::new($this->_stmt); 362 } 363 364 if ($fetchMode === FetchMode::NUMERIC) { 365 return $values; 366 } 367 368 assert(is_array($this->_columnNames)); 369 $assoc = array_combine($this->_columnNames, $values); 370 assert(is_array($assoc)); 371 372 switch ($fetchMode) { 373 case FetchMode::ASSOCIATIVE: 374 return $assoc; 375 376 case FetchMode::MIXED: 377 return $assoc + $values; 378 379 case FetchMode::STANDARD_OBJECT: 380 return (object) $assoc; 381 382 default: 383 throw new MysqliException(sprintf("Unknown fetch type '%s'", $fetchMode)); 384 } 385 } 386 387 /** 388 * {@inheritdoc} 389 * 390 * @deprecated Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. 391 */ 392 public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) 393 { 394 $fetchMode = $fetchMode ?: $this->_defaultFetchMode; 395 396 $rows = []; 397 398 if ($fetchMode === FetchMode::COLUMN) { 399 while (($row = $this->fetchColumn()) !== false) { 400 $rows[] = $row; 401 } 402 } else { 403 while (($row = $this->fetch($fetchMode)) !== false) { 404 $rows[] = $row; 405 } 406 } 407 408 return $rows; 409 } 410 411 /** 412 * {@inheritdoc} 413 * 414 * @deprecated Use fetchOne() instead. 415 */ 416 public function fetchColumn($columnIndex = 0) 417 { 418 $row = $this->fetch(FetchMode::NUMERIC); 419 420 if ($row === false) { 421 return false; 422 } 423 424 return $row[$columnIndex] ?? null; 425 } 426 427 /** 428 * {@inheritdoc} 429 * 430 * @deprecated The error information is available via exceptions. 431 */ 432 public function fetchNumeric() 433 { 434 // do not try fetching from the statement if it's not expected to contain the result 435 // in order to prevent exceptional situation 436 if (! $this->result) { 437 return false; 438 } 439 440 $values = $this->_fetch(); 441 442 if ($values === null) { 443 return false; 444 } 445 446 if ($values === false) { 447 throw StatementError::new($this->_stmt); 448 } 449 450 return $values; 451 } 452 453 /** 454 * {@inheritDoc} 455 */ 456 public function fetchAssociative() 457 { 458 $values = $this->fetchNumeric(); 459 460 if ($values === false) { 461 return false; 462 } 463 464 assert(is_array($this->_columnNames)); 465 $row = array_combine($this->_columnNames, $values); 466 assert(is_array($row)); 467 468 return $row; 469 } 470 471 /** 472 * {@inheritdoc} 473 */ 474 public function fetchOne() 475 { 476 return FetchUtils::fetchOne($this); 477 } 478 479 /** 480 * {@inheritdoc} 481 */ 482 public function fetchAllNumeric(): array 483 { 484 return FetchUtils::fetchAllNumeric($this); 485 } 486 487 /** 488 * {@inheritdoc} 489 */ 490 public function fetchAllAssociative(): array 491 { 492 return FetchUtils::fetchAllAssociative($this); 493 } 494 495 /** 496 * {@inheritdoc} 497 */ 498 public function fetchFirstColumn(): array 499 { 500 return FetchUtils::fetchFirstColumn($this); 501 } 502 503 /** 504 * {@inheritdoc} 505 */ 506 public function errorCode() 507 { 508 return $this->_stmt->errno; 509 } 510 511 /** 512 * {@inheritdoc} 513 * 514 * @deprecated The error information is available via exceptions. 515 * 516 * @return string 517 */ 518 public function errorInfo() 519 { 520 return $this->_stmt->error; 521 } 522 523 /** 524 * {@inheritdoc} 525 * 526 * @deprecated Use free() instead. 527 */ 528 public function closeCursor() 529 { 530 $this->free(); 531 532 return true; 533 } 534 535 /** 536 * {@inheritdoc} 537 */ 538 public function rowCount() 539 { 540 if ($this->_columnNames === false) { 541 return $this->_stmt->affected_rows; 542 } 543 544 return $this->_stmt->num_rows; 545 } 546 547 /** 548 * {@inheritdoc} 549 */ 550 public function columnCount() 551 { 552 return $this->_stmt->field_count; 553 } 554 555 public function free(): void 556 { 557 $this->_stmt->free_result(); 558 $this->result = false; 559 } 560 561 /** 562 * {@inheritdoc} 563 * 564 * @deprecated Use one of the fetch- or iterate-related methods. 565 */ 566 public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) 567 { 568 $this->_defaultFetchMode = $fetchMode; 569 570 return true; 571 } 572 573 /** 574 * {@inheritdoc} 575 * 576 * @deprecated Use iterateNumeric(), iterateAssociative() or iterateColumn() instead. 577 */ 578 #[ReturnTypeWillChange] 579 public function getIterator() 580 { 581 return new StatementIterator($this); 582 } 583} 584