1<?php 2 3namespace Doctrine\DBAL\Cache; 4 5use ArrayIterator; 6use Doctrine\Common\Cache\Cache; 7use Doctrine\DBAL\Driver\ResultStatement; 8use Doctrine\DBAL\Driver\Statement; 9use Doctrine\DBAL\FetchMode; 10use InvalidArgumentException; 11use IteratorAggregate; 12use PDO; 13use function array_merge; 14use function array_values; 15use function reset; 16 17/** 18 * Cache statement for SQL results. 19 * 20 * A result is saved in multiple cache keys, there is the originally specified 21 * cache key which is just pointing to result rows by key. The following things 22 * have to be ensured: 23 * 24 * 1. lifetime of the original key has to be longer than that of all the individual rows keys 25 * 2. if any one row key is missing the query has to be re-executed. 26 * 27 * Also you have to realize that the cache will load the whole result into memory at once to ensure 2. 28 * This means that the memory usage for cached results might increase by using this feature. 29 */ 30class ResultCacheStatement implements IteratorAggregate, ResultStatement 31{ 32 /** @var Cache */ 33 private $resultCache; 34 35 /** @var string */ 36 private $cacheKey; 37 38 /** @var string */ 39 private $realKey; 40 41 /** @var int */ 42 private $lifetime; 43 44 /** @var Statement */ 45 private $statement; 46 47 /** 48 * Did we reach the end of the statement? 49 * 50 * @var bool 51 */ 52 private $emptied = false; 53 54 /** @var mixed[] */ 55 private $data; 56 57 /** @var int */ 58 private $defaultFetchMode = FetchMode::MIXED; 59 60 /** 61 * @param string $cacheKey 62 * @param string $realKey 63 * @param int $lifetime 64 */ 65 public function __construct(Statement $stmt, Cache $resultCache, $cacheKey, $realKey, $lifetime) 66 { 67 $this->statement = $stmt; 68 $this->resultCache = $resultCache; 69 $this->cacheKey = $cacheKey; 70 $this->realKey = $realKey; 71 $this->lifetime = $lifetime; 72 } 73 74 /** 75 * {@inheritdoc} 76 */ 77 public function closeCursor() 78 { 79 $this->statement->closeCursor(); 80 if (! $this->emptied || $this->data === null) { 81 return true; 82 } 83 84 $data = $this->resultCache->fetch($this->cacheKey); 85 if (! $data) { 86 $data = []; 87 } 88 $data[$this->realKey] = $this->data; 89 90 $this->resultCache->save($this->cacheKey, $data, $this->lifetime); 91 unset($this->data); 92 93 return true; 94 } 95 96 /** 97 * {@inheritdoc} 98 */ 99 public function columnCount() 100 { 101 return $this->statement->columnCount(); 102 } 103 104 /** 105 * {@inheritdoc} 106 */ 107 public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) 108 { 109 $this->defaultFetchMode = $fetchMode; 110 111 return true; 112 } 113 114 /** 115 * {@inheritdoc} 116 */ 117 public function getIterator() 118 { 119 $data = $this->fetchAll(); 120 121 return new ArrayIterator($data); 122 } 123 124 /** 125 * {@inheritdoc} 126 */ 127 public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) 128 { 129 if ($this->data === null) { 130 $this->data = []; 131 } 132 133 $row = $this->statement->fetch(FetchMode::ASSOCIATIVE); 134 135 if ($row) { 136 $this->data[] = $row; 137 138 $fetchMode = $fetchMode ?: $this->defaultFetchMode; 139 140 if ($fetchMode === FetchMode::ASSOCIATIVE) { 141 return $row; 142 } 143 144 if ($fetchMode === FetchMode::NUMERIC) { 145 return array_values($row); 146 } 147 148 if ($fetchMode === FetchMode::MIXED) { 149 return array_merge($row, array_values($row)); 150 } 151 152 if ($fetchMode === FetchMode::COLUMN) { 153 return reset($row); 154 } 155 156 throw new InvalidArgumentException('Invalid fetch-style given for caching result.'); 157 } 158 159 $this->emptied = true; 160 161 return false; 162 } 163 164 /** 165 * {@inheritdoc} 166 */ 167 public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) 168 { 169 $data = $this->statement->fetchAll($fetchMode, $fetchArgument, $ctorArgs); 170 171 if ($fetchMode === FetchMode::COLUMN) { 172 foreach ($data as $key => $value) { 173 $data[$key] = [$value]; 174 } 175 } 176 177 $this->data = $data; 178 $this->emptied = true; 179 180 return $this->data; 181 } 182 183 /** 184 * {@inheritdoc} 185 */ 186 public function fetchColumn($columnIndex = 0) 187 { 188 $row = $this->fetch(FetchMode::NUMERIC); 189 190 // TODO: verify that return false is the correct behavior 191 return $row[$columnIndex] ?? false; 192 } 193 194 /** 195 * Returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement 196 * executed by the corresponding object. 197 * 198 * If the last SQL statement executed by the associated Statement object was a SELECT statement, 199 * some databases may return the number of rows returned by that statement. However, 200 * this behaviour is not guaranteed for all databases and should not be 201 * relied on for portable applications. 202 * 203 * @return int The number of rows. 204 */ 205 public function rowCount() 206 { 207 return $this->statement->rowCount(); 208 } 209} 210