StatementPrefetch.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. <?php
  2. namespace Drupal\Core\Database;
  3. /**
  4. * An implementation of StatementInterface that prefetches all data.
  5. *
  6. * This class behaves very similar to a \PDOStatement but as it always fetches
  7. * every row it is possible to manipulate those results.
  8. */
  9. class StatementPrefetch implements \Iterator, StatementInterface {
  10. /**
  11. * The query string.
  12. *
  13. * @var string
  14. */
  15. protected $queryString;
  16. /**
  17. * Driver-specific options. Can be used by child classes.
  18. *
  19. * @var array
  20. */
  21. protected $driverOptions;
  22. /**
  23. * Reference to the Drupal database connection object for this statement.
  24. *
  25. * @var \Drupal\Core\Database\Connection
  26. */
  27. public $dbh;
  28. /**
  29. * Reference to the PDO connection object for this statement.
  30. *
  31. * @var \PDO
  32. */
  33. protected $pdoConnection;
  34. /**
  35. * Main data store.
  36. *
  37. * @var array
  38. */
  39. protected $data = [];
  40. /**
  41. * The current row, retrieved in \PDO::FETCH_ASSOC format.
  42. *
  43. * @var array
  44. */
  45. protected $currentRow = NULL;
  46. /**
  47. * The key of the current row.
  48. *
  49. * @var int
  50. */
  51. protected $currentKey = NULL;
  52. /**
  53. * The list of column names in this result set.
  54. *
  55. * @var array
  56. */
  57. protected $columnNames = NULL;
  58. /**
  59. * The number of rows affected by the last query.
  60. *
  61. * @var int
  62. */
  63. protected $rowCount = NULL;
  64. /**
  65. * The number of rows in this result set.
  66. *
  67. * @var int
  68. */
  69. protected $resultRowCount = 0;
  70. /**
  71. * Holds the current fetch style (which will be used by the next fetch).
  72. * @see \PDOStatement::fetch()
  73. *
  74. * @var int
  75. */
  76. protected $fetchStyle = \PDO::FETCH_OBJ;
  77. /**
  78. * Holds supplementary current fetch options (which will be used by the next fetch).
  79. *
  80. * @var array
  81. */
  82. protected $fetchOptions = [
  83. 'class' => 'stdClass',
  84. 'constructor_args' => [],
  85. 'object' => NULL,
  86. 'column' => 0,
  87. ];
  88. /**
  89. * Holds the default fetch style.
  90. *
  91. * @var int
  92. */
  93. protected $defaultFetchStyle = \PDO::FETCH_OBJ;
  94. /**
  95. * Holds supplementary default fetch options.
  96. *
  97. * @var array
  98. */
  99. protected $defaultFetchOptions = [
  100. 'class' => 'stdClass',
  101. 'constructor_args' => [],
  102. 'object' => NULL,
  103. 'column' => 0,
  104. ];
  105. /**
  106. * Is rowCount() execution allowed.
  107. *
  108. * @var bool
  109. */
  110. public $allowRowCount = FALSE;
  111. public function __construct(\PDO $pdo_connection, Connection $connection, $query, array $driver_options = []) {
  112. $this->pdoConnection = $pdo_connection;
  113. $this->dbh = $connection;
  114. $this->queryString = $query;
  115. $this->driverOptions = $driver_options;
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function execute($args = [], $options = []) {
  121. if (isset($options['fetch'])) {
  122. if (is_string($options['fetch'])) {
  123. // Default to an object. Note: db fields will be added to the object
  124. // before the constructor is run. If you need to assign fields after
  125. // the constructor is run. See https://www.drupal.org/node/315092.
  126. $this->setFetchMode(\PDO::FETCH_CLASS, $options['fetch']);
  127. }
  128. else {
  129. $this->setFetchMode($options['fetch']);
  130. }
  131. }
  132. $logger = $this->dbh->getLogger();
  133. if (!empty($logger)) {
  134. $query_start = microtime(TRUE);
  135. }
  136. // Prepare the query.
  137. $statement = $this->getStatement($this->queryString, $args);
  138. if (!$statement) {
  139. $this->throwPDOException();
  140. }
  141. $return = $statement->execute($args);
  142. if (!$return) {
  143. $this->throwPDOException();
  144. }
  145. if ($options['return'] == Database::RETURN_AFFECTED) {
  146. $this->rowCount = $statement->rowCount();
  147. }
  148. // Fetch all the data from the reply, in order to release any lock
  149. // as soon as possible.
  150. $this->data = $statement->fetchAll(\PDO::FETCH_ASSOC);
  151. // Destroy the statement as soon as possible. See the documentation of
  152. // \Drupal\Core\Database\Driver\sqlite\Statement for an explanation.
  153. unset($statement);
  154. $this->resultRowCount = count($this->data);
  155. if ($this->resultRowCount) {
  156. $this->columnNames = array_keys($this->data[0]);
  157. }
  158. else {
  159. $this->columnNames = [];
  160. }
  161. if (!empty($logger)) {
  162. $query_end = microtime(TRUE);
  163. $logger->log($this, $args, $query_end - $query_start);
  164. }
  165. // Initialize the first row in $this->currentRow.
  166. $this->next();
  167. return $return;
  168. }
  169. /**
  170. * Throw a PDO Exception based on the last PDO error.
  171. */
  172. protected function throwPDOException() {
  173. $error_info = $this->dbh->errorInfo();
  174. // We rebuild a message formatted in the same way as PDO.
  175. $exception = new \PDOException("SQLSTATE[" . $error_info[0] . "]: General error " . $error_info[1] . ": " . $error_info[2]);
  176. $exception->errorInfo = $error_info;
  177. throw $exception;
  178. }
  179. /**
  180. * Grab a PDOStatement object from a given query and its arguments.
  181. *
  182. * Some drivers (including SQLite) will need to perform some preparation
  183. * themselves to get the statement right.
  184. *
  185. * @param $query
  186. * The query.
  187. * @param array|null $args
  188. * An array of arguments. This can be NULL.
  189. *
  190. * @return \PDOStatement
  191. * A PDOStatement object.
  192. */
  193. protected function getStatement($query, &$args = []) {
  194. return $this->dbh->prepare($query);
  195. }
  196. /**
  197. * {@inheritdoc}
  198. */
  199. public function getQueryString() {
  200. return $this->queryString;
  201. }
  202. /**
  203. * {@inheritdoc}
  204. */
  205. public function setFetchMode($mode, $a1 = NULL, $a2 = []) {
  206. $this->defaultFetchStyle = $mode;
  207. switch ($mode) {
  208. case \PDO::FETCH_CLASS:
  209. $this->defaultFetchOptions['class'] = $a1;
  210. if ($a2) {
  211. $this->defaultFetchOptions['constructor_args'] = $a2;
  212. }
  213. break;
  214. case \PDO::FETCH_COLUMN:
  215. $this->defaultFetchOptions['column'] = $a1;
  216. break;
  217. case \PDO::FETCH_INTO:
  218. $this->defaultFetchOptions['object'] = $a1;
  219. break;
  220. }
  221. // Set the values for the next fetch.
  222. $this->fetchStyle = $this->defaultFetchStyle;
  223. $this->fetchOptions = $this->defaultFetchOptions;
  224. }
  225. /**
  226. * Return the current row formatted according to the current fetch style.
  227. *
  228. * This is the core method of this class. It grabs the value at the current
  229. * array position in $this->data and format it according to $this->fetchStyle
  230. * and $this->fetchMode.
  231. *
  232. * @return mixed
  233. * The current row formatted as requested.
  234. */
  235. public function current() {
  236. if (isset($this->currentRow)) {
  237. switch ($this->fetchStyle) {
  238. case \PDO::FETCH_ASSOC:
  239. return $this->currentRow;
  240. case \PDO::FETCH_BOTH:
  241. // \PDO::FETCH_BOTH returns an array indexed by both the column name
  242. // and the column number.
  243. return $this->currentRow + array_values($this->currentRow);
  244. case \PDO::FETCH_NUM:
  245. return array_values($this->currentRow);
  246. case \PDO::FETCH_LAZY:
  247. // We do not do lazy as everything is fetched already. Fallback to
  248. // \PDO::FETCH_OBJ.
  249. case \PDO::FETCH_OBJ:
  250. return (object) $this->currentRow;
  251. case \PDO::FETCH_CLASS | \PDO::FETCH_CLASSTYPE:
  252. $class_name = array_shift($this->currentRow);
  253. // Deliberate no break.
  254. case \PDO::FETCH_CLASS:
  255. if (!isset($class_name)) {
  256. $class_name = $this->fetchOptions['class'];
  257. }
  258. if (count($this->fetchOptions['constructor_args'])) {
  259. $reflector = new \ReflectionClass($class_name);
  260. $result = $reflector->newInstanceArgs($this->fetchOptions['constructor_args']);
  261. }
  262. else {
  263. $result = new $class_name();
  264. }
  265. foreach ($this->currentRow as $k => $v) {
  266. $result->$k = $v;
  267. }
  268. return $result;
  269. case \PDO::FETCH_INTO:
  270. foreach ($this->currentRow as $k => $v) {
  271. $this->fetchOptions['object']->$k = $v;
  272. }
  273. return $this->fetchOptions['object'];
  274. case \PDO::FETCH_COLUMN:
  275. if (isset($this->columnNames[$this->fetchOptions['column']])) {
  276. return $this->currentRow[$this->columnNames[$this->fetchOptions['column']]];
  277. }
  278. else {
  279. return;
  280. }
  281. }
  282. }
  283. }
  284. /**
  285. * {@inheritdoc}
  286. */
  287. public function key() {
  288. return $this->currentKey;
  289. }
  290. /**
  291. * {@inheritdoc}
  292. */
  293. public function rewind() {
  294. // Nothing to do: our DatabaseStatement can't be rewound.
  295. }
  296. /**
  297. * {@inheritdoc}
  298. */
  299. public function next() {
  300. if (!empty($this->data)) {
  301. $this->currentRow = reset($this->data);
  302. $this->currentKey = key($this->data);
  303. unset($this->data[$this->currentKey]);
  304. }
  305. else {
  306. $this->currentRow = NULL;
  307. }
  308. }
  309. /**
  310. * {@inheritdoc}
  311. */
  312. public function valid() {
  313. return isset($this->currentRow);
  314. }
  315. /**
  316. * {@inheritdoc}
  317. */
  318. public function rowCount() {
  319. // SELECT query should not use the method.
  320. if ($this->allowRowCount) {
  321. return $this->rowCount;
  322. }
  323. else {
  324. throw new RowCountException();
  325. }
  326. }
  327. /**
  328. * {@inheritdoc}
  329. */
  330. public function fetch($fetch_style = NULL, $cursor_orientation = \PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
  331. if (isset($this->currentRow)) {
  332. // Set the fetch parameter.
  333. $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
  334. $this->fetchOptions = $this->defaultFetchOptions;
  335. // Grab the row in the format specified above.
  336. $return = $this->current();
  337. // Advance the cursor.
  338. $this->next();
  339. // Reset the fetch parameters to the value stored using setFetchMode().
  340. $this->fetchStyle = $this->defaultFetchStyle;
  341. $this->fetchOptions = $this->defaultFetchOptions;
  342. return $return;
  343. }
  344. else {
  345. return FALSE;
  346. }
  347. }
  348. public function fetchColumn($index = 0) {
  349. if (isset($this->currentRow) && isset($this->columnNames[$index])) {
  350. // We grab the value directly from $this->data, and format it.
  351. $return = $this->currentRow[$this->columnNames[$index]];
  352. $this->next();
  353. return $return;
  354. }
  355. else {
  356. return FALSE;
  357. }
  358. }
  359. /**
  360. * {@inheritdoc}
  361. */
  362. public function fetchField($index = 0) {
  363. return $this->fetchColumn($index);
  364. }
  365. /**
  366. * {@inheritdoc}
  367. */
  368. public function fetchObject($class_name = NULL, $constructor_args = []) {
  369. if (isset($this->currentRow)) {
  370. if (!isset($class_name)) {
  371. // Directly cast to an object to avoid a function call.
  372. $result = (object) $this->currentRow;
  373. }
  374. else {
  375. $this->fetchStyle = \PDO::FETCH_CLASS;
  376. $this->fetchOptions = [
  377. 'class' => $class_name,
  378. 'constructor_args' => $constructor_args,
  379. ];
  380. // Grab the row in the format specified above.
  381. $result = $this->current();
  382. // Reset the fetch parameters to the value stored using setFetchMode().
  383. $this->fetchStyle = $this->defaultFetchStyle;
  384. $this->fetchOptions = $this->defaultFetchOptions;
  385. }
  386. $this->next();
  387. return $result;
  388. }
  389. else {
  390. return FALSE;
  391. }
  392. }
  393. /**
  394. * {@inheritdoc}
  395. */
  396. public function fetchAssoc() {
  397. if (isset($this->currentRow)) {
  398. $result = $this->currentRow;
  399. $this->next();
  400. return $result;
  401. }
  402. else {
  403. return FALSE;
  404. }
  405. }
  406. /**
  407. * {@inheritdoc}
  408. */
  409. public function fetchAll($mode = NULL, $column_index = NULL, $constructor_arguments = NULL) {
  410. $this->fetchStyle = isset($mode) ? $mode : $this->defaultFetchStyle;
  411. $this->fetchOptions = $this->defaultFetchOptions;
  412. if (isset($column_index)) {
  413. $this->fetchOptions['column'] = $column_index;
  414. }
  415. if (isset($constructor_arguments)) {
  416. $this->fetchOptions['constructor_args'] = $constructor_arguments;
  417. }
  418. $result = [];
  419. // Traverse the array as PHP would have done.
  420. while (isset($this->currentRow)) {
  421. // Grab the row in the format specified above.
  422. $result[] = $this->current();
  423. $this->next();
  424. }
  425. // Reset the fetch parameters to the value stored using setFetchMode().
  426. $this->fetchStyle = $this->defaultFetchStyle;
  427. $this->fetchOptions = $this->defaultFetchOptions;
  428. return $result;
  429. }
  430. /**
  431. * {@inheritdoc}
  432. */
  433. public function fetchCol($index = 0) {
  434. if (isset($this->columnNames[$index])) {
  435. $result = [];
  436. // Traverse the array as PHP would have done.
  437. while (isset($this->currentRow)) {
  438. $result[] = $this->currentRow[$this->columnNames[$index]];
  439. $this->next();
  440. }
  441. return $result;
  442. }
  443. else {
  444. return [];
  445. }
  446. }
  447. /**
  448. * {@inheritdoc}
  449. */
  450. public function fetchAllKeyed($key_index = 0, $value_index = 1) {
  451. if (!isset($this->columnNames[$key_index]) || !isset($this->columnNames[$value_index])) {
  452. return [];
  453. }
  454. $key = $this->columnNames[$key_index];
  455. $value = $this->columnNames[$value_index];
  456. $result = [];
  457. // Traverse the array as PHP would have done.
  458. while (isset($this->currentRow)) {
  459. $result[$this->currentRow[$key]] = $this->currentRow[$value];
  460. $this->next();
  461. }
  462. return $result;
  463. }
  464. /**
  465. * {@inheritdoc}
  466. */
  467. public function fetchAllAssoc($key, $fetch_style = NULL) {
  468. $this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
  469. $this->fetchOptions = $this->defaultFetchOptions;
  470. $result = [];
  471. // Traverse the array as PHP would have done.
  472. while (isset($this->currentRow)) {
  473. // Grab the row in its raw \PDO::FETCH_ASSOC format.
  474. $result_row = $this->current();
  475. $result[$this->currentRow[$key]] = $result_row;
  476. $this->next();
  477. }
  478. // Reset the fetch parameters to the value stored using setFetchMode().
  479. $this->fetchStyle = $this->defaultFetchStyle;
  480. $this->fetchOptions = $this->defaultFetchOptions;
  481. return $result;
  482. }
  483. }