1<?php 2 3namespace Doctrine\Common\Cache; 4 5use SQLite3; 6use SQLite3Result; 7use const SQLITE3_ASSOC; 8use const SQLITE3_BLOB; 9use const SQLITE3_TEXT; 10use function array_search; 11use function implode; 12use function serialize; 13use function sprintf; 14use function time; 15use function unserialize; 16 17/** 18 * SQLite3 cache provider. 19 */ 20class SQLite3Cache extends CacheProvider 21{ 22 /** 23 * The ID field will store the cache key. 24 */ 25 public const ID_FIELD = 'k'; 26 27 /** 28 * The data field will store the serialized PHP value. 29 */ 30 public const DATA_FIELD = 'd'; 31 32 /** 33 * The expiration field will store a date value indicating when the 34 * cache entry should expire. 35 */ 36 public const EXPIRATION_FIELD = 'e'; 37 38 /** @var SQLite3 */ 39 private $sqlite; 40 41 /** @var string */ 42 private $table; 43 44 /** 45 * Calling the constructor will ensure that the database file and table 46 * exist and will create both if they don't. 47 * 48 * @param string $table 49 */ 50 public function __construct(SQLite3 $sqlite, $table) 51 { 52 $this->sqlite = $sqlite; 53 $this->table = (string) $table; 54 55 $this->ensureTableExists(); 56 } 57 58 private function ensureTableExists() : void 59 { 60 $this->sqlite->exec( 61 sprintf( 62 'CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY NOT NULL, %s BLOB, %s INTEGER)', 63 $this->table, 64 static::ID_FIELD, 65 static::DATA_FIELD, 66 static::EXPIRATION_FIELD 67 ) 68 ); 69 } 70 71 /** 72 * {@inheritdoc} 73 */ 74 protected function doFetch($id) 75 { 76 $item = $this->findById($id); 77 78 if (! $item) { 79 return false; 80 } 81 82 return unserialize($item[self::DATA_FIELD]); 83 } 84 85 /** 86 * {@inheritdoc} 87 */ 88 protected function doContains($id) 89 { 90 return $this->findById($id, false) !== null; 91 } 92 93 /** 94 * {@inheritdoc} 95 */ 96 protected function doSave($id, $data, $lifeTime = 0) 97 { 98 $statement = $this->sqlite->prepare(sprintf( 99 'INSERT OR REPLACE INTO %s (%s) VALUES (:id, :data, :expire)', 100 $this->table, 101 implode(',', $this->getFields()) 102 )); 103 104 $statement->bindValue(':id', $id); 105 $statement->bindValue(':data', serialize($data), SQLITE3_BLOB); 106 $statement->bindValue(':expire', $lifeTime > 0 ? time() + $lifeTime : null); 107 108 return $statement->execute() instanceof SQLite3Result; 109 } 110 111 /** 112 * {@inheritdoc} 113 */ 114 protected function doDelete($id) 115 { 116 [$idField] = $this->getFields(); 117 118 $statement = $this->sqlite->prepare(sprintf( 119 'DELETE FROM %s WHERE %s = :id', 120 $this->table, 121 $idField 122 )); 123 124 $statement->bindValue(':id', $id); 125 126 return $statement->execute() instanceof SQLite3Result; 127 } 128 129 /** 130 * {@inheritdoc} 131 */ 132 protected function doFlush() 133 { 134 return $this->sqlite->exec(sprintf('DELETE FROM %s', $this->table)); 135 } 136 137 /** 138 * {@inheritdoc} 139 */ 140 protected function doGetStats() 141 { 142 // no-op. 143 } 144 145 /** 146 * Find a single row by ID. 147 * 148 * @param mixed $id 149 * 150 * @return array|null 151 */ 152 private function findById($id, bool $includeData = true) : ?array 153 { 154 [$idField] = $fields = $this->getFields(); 155 156 if (! $includeData) { 157 $key = array_search(static::DATA_FIELD, $fields); 158 unset($fields[$key]); 159 } 160 161 $statement = $this->sqlite->prepare(sprintf( 162 'SELECT %s FROM %s WHERE %s = :id LIMIT 1', 163 implode(',', $fields), 164 $this->table, 165 $idField 166 )); 167 168 $statement->bindValue(':id', $id, SQLITE3_TEXT); 169 170 $item = $statement->execute()->fetchArray(SQLITE3_ASSOC); 171 172 if ($item === false) { 173 return null; 174 } 175 176 if ($this->isExpired($item)) { 177 $this->doDelete($id); 178 179 return null; 180 } 181 182 return $item; 183 } 184 185 /** 186 * Gets an array of the fields in our table. 187 * 188 * @return array 189 */ 190 private function getFields() : array 191 { 192 return [static::ID_FIELD, static::DATA_FIELD, static::EXPIRATION_FIELD]; 193 } 194 195 /** 196 * Check if the item is expired. 197 * 198 * @param array $item 199 */ 200 private function isExpired(array $item) : bool 201 { 202 return isset($item[static::EXPIRATION_FIELD]) && 203 $item[self::EXPIRATION_FIELD] !== null && 204 $item[self::EXPIRATION_FIELD] < time(); 205 } 206} 207