SQLite3Cache.php 4.5 KB

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