SqliteDriver.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Cache\Driver;
  12. use Carbon\Carbon;
  13. use Hyperf\Coordinator\Timer;
  14. use Hyperf\Pool\SimplePool\Pool;
  15. use Hyperf\Pool\SimplePool\PoolFactory;
  16. use InvalidArgumentException;
  17. use PDO;
  18. use Psr\Container\ContainerInterface;
  19. class SqliteDriver extends Driver
  20. {
  21. protected ?Pool $pool = null;
  22. protected ?Timer $timer = null;
  23. protected string $table;
  24. protected bool $tableCreated = false;
  25. public function __construct(ContainerInterface $container, array $config)
  26. {
  27. // @todo check if the sqlite pdo extension is installed and hooked
  28. $config = array_replace([
  29. 'database' => ':memory:',
  30. 'table' => 'hyperf_cache' . uniqid('_'),
  31. 'prefix' => '',
  32. 'max_connections' => 10,
  33. 'options' => [],
  34. ], $config);
  35. parent::__construct($container, $config);
  36. $this->table = $config['table'];
  37. }
  38. public function fetch(string $key, $default = null): array
  39. {
  40. return $this->execute(function (PDO $pdo) use ($key, $default) {
  41. $sql = sprintf('SELECT value, expiration FROM %s WHERE id = ?', $this->table);
  42. $stmt = $pdo->prepare($sql);
  43. $stmt->execute([$this->getCacheKey($key)]);
  44. $result = $stmt->fetch(PDO::FETCH_ASSOC);
  45. return $result === false ? [false, $default] : [true, $this->packer->unpack($result['value'])];
  46. });
  47. }
  48. public function clearExpired()
  49. {
  50. return $this->execute(function (PDO $pdo) {
  51. $sql = sprintf('DELETE FROM %s WHERE expiration > 0 AND expiration < ?', $this->table);
  52. $stmt = $pdo->prepare($sql);
  53. return $stmt->execute([$this->currentTime()]);
  54. });
  55. }
  56. public function clearPrefix(string $prefix): bool
  57. {
  58. return $this->execute(function (PDO $pdo) use ($prefix) {
  59. $sql = sprintf('DELETE FROM %s WHERE id LIKE ?', $this->table);
  60. $stmt = $pdo->prepare($sql);
  61. return $stmt->execute([$this->getCacheKey($prefix . '%')]);
  62. });
  63. }
  64. public function get($key, $default = null): mixed
  65. {
  66. return $this->execute(function (PDO $pdo) use ($key, $default) {
  67. $sql = sprintf('SELECT value, expiration FROM %s WHERE id = ?', $this->table);
  68. $stmt = $pdo->prepare($sql);
  69. $stmt->execute([$this->getCacheKey($key)]);
  70. $result = $stmt->fetch(PDO::FETCH_ASSOC);
  71. return $result === false ? $default : $this->packer->unpack($result['value']);
  72. });
  73. }
  74. public function set($key, $value, $ttl = null): bool
  75. {
  76. return $this->execute(function (PDO $pdo) use ($key, $value, $ttl) {
  77. $seconds = $this->secondsUntil($ttl);
  78. $sql = sprintf('INSERT OR REPLACE INTO %s (id, value, expiration) VALUES (?, ?, ?)', $this->table);
  79. $stmt = $pdo->prepare($sql);
  80. return $stmt->execute([
  81. $this->getCacheKey($key),
  82. $this->packer->pack($value),
  83. $seconds > 0 ? Carbon::now()->addSeconds($seconds)->timestamp : 0,
  84. ]);
  85. });
  86. }
  87. public function delete($key): bool
  88. {
  89. return $this->execute(function (PDO $pdo) use ($key) {
  90. $sql = sprintf('DELETE FROM %s WHERE id = ?', $this->table);
  91. $stmt = $pdo->prepare($sql);
  92. return $stmt->execute([$this->getCacheKey($key)]);
  93. });
  94. }
  95. public function clear(): bool
  96. {
  97. return $this->execute(function (PDO $pdo) {
  98. $sql = sprintf('DELETE FROM %s', $this->table);
  99. $stmt = $pdo->prepare($sql);
  100. return $stmt->execute();
  101. });
  102. }
  103. public function getMultiple($keys, $default = null): iterable
  104. {
  105. return $this->execute(function (PDO $pdo) use ($keys, $default) {
  106. $sql = sprintf('SELECT id, value, expiration FROM %s WHERE id IN (%s)', $this->table, implode(', ', array_fill(0, count($keys), '?')));
  107. $stmt = $pdo->prepare($sql);
  108. $stmt->execute(array_map(fn ($key) => $this->getCacheKey($key), $keys));
  109. $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
  110. $values = [];
  111. foreach ($keys as $key) {
  112. $values[$key] = $default;
  113. }
  114. foreach ($result as $item) {
  115. $values[$item['key']] = $this->packer->unpack($item['value']);
  116. }
  117. return $values;
  118. });
  119. }
  120. public function setMultiple($values, $ttl = null): bool
  121. {
  122. return $this->execute(function (PDO $pdo) use ($values, $ttl) {
  123. $seconds = $this->secondsUntil($ttl);
  124. $sql = sprintf('INSERT OR REPLACE INTO %s (id, value, expiration) VALUES (?, ?, ?)', $this->table);
  125. $stmt = $pdo->prepare($sql);
  126. foreach ($values as $key => $value) {
  127. $stmt->execute([
  128. $this->getCacheKey($key),
  129. $this->packer->pack($value),
  130. $seconds > 0 ? Carbon::now()->addSeconds($seconds)->timestamp : 0,
  131. ]);
  132. }
  133. return true;
  134. });
  135. }
  136. public function deleteMultiple($keys): bool
  137. {
  138. return $this->execute(function (PDO $pdo) use ($keys) {
  139. $sql = sprintf('DELETE FROM %s WHERE id IN (%s)', $this->table, implode(', ', array_fill(0, count($keys), '?')));
  140. $stmt = $pdo->prepare($sql);
  141. return $stmt->execute(array_map(fn ($key) => $this->getCacheKey($key), $keys));
  142. });
  143. }
  144. public function has($key): bool
  145. {
  146. return $this->execute(function (PDO $pdo) use ($key) {
  147. $sql = sprintf('SELECT 1 FROM %s WHERE id = ?', $this->table);
  148. $stmt = $pdo->prepare($sql);
  149. $stmt->execute([$this->getCacheKey($key)]);
  150. return $stmt->fetch(PDO::FETCH_ASSOC) !== false;
  151. });
  152. }
  153. public function dump()
  154. {
  155. dump($this->execute(function (PDO $pdo) {
  156. $sql = sprintf('SELECT * FROM %s', $this->table);
  157. $stmt = $pdo->prepare($sql);
  158. $stmt->execute();
  159. return $stmt->fetchAll(PDO::FETCH_ASSOC);
  160. }));
  161. }
  162. protected function connect(array $config): PDO
  163. {
  164. $options = array_replace([
  165. PDO::ATTR_CASE => PDO::CASE_NATURAL,
  166. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  167. PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
  168. PDO::ATTR_STRINGIFY_FETCHES => false,
  169. PDO::ATTR_EMULATE_PREPARES => false,
  170. ], $config['options'] ?? []);
  171. if ($config['database'] === ':memory:') {
  172. return $this->createConnection('sqlite::memory:', $config, $options);
  173. }
  174. $path = realpath($config['database']);
  175. if ($path === false) {
  176. throw new InvalidArgumentException("Database ({$config['database']}) does not exist.");
  177. }
  178. return $this->createConnection("sqlite:{$path}", $config, $options);
  179. }
  180. protected function createConnection($dsn, array $config, array $options): PDO
  181. {
  182. return new PDO($dsn, null, null, $options);
  183. }
  184. protected function createTable(PDO $pdo): void
  185. {
  186. $creation = <<<SQL
  187. CREATE TABLE IF NOT EXISTS {$this->table} (
  188. id TEXT PRIMARY KEY NOT NULL,
  189. value TEXT NOT NULL,
  190. expiration INTEGER NOT NULL
  191. );
  192. CREATE INDEX IF NOT EXISTS expiration_index ON {$this->table} (expiration)
  193. SQL;
  194. $pdo->exec($creation);
  195. }
  196. protected function execute(callable $callback)
  197. {
  198. if (! $this->pool) {
  199. $factory = $this->container->get(PoolFactory::class);
  200. $config = $this->config;
  201. $this->pool = $factory->get(static::class . '.pool', fn () => $this->connect($config), [
  202. 'max_connections' => (int) ($config['max_connections'] ?? 10),
  203. ]);
  204. }
  205. $connection = $this->pool->get();
  206. $pdo = $connection->getConnection();
  207. if (! $this->tableCreated) {
  208. $this->createTable($pdo);
  209. $this->timer ??= new Timer();
  210. $this->timer->tick(1, fn () => $this->clearExpired());
  211. $this->tableCreated = true;
  212. }
  213. try {
  214. return $callback($pdo);
  215. } finally {
  216. $connection->release();
  217. }
  218. }
  219. }