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