AbstractAdapter.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Cache\Adapter;
  11. use Psr\Log\LoggerAwareInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Component\Cache\CacheItem;
  14. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  15. use Symfony\Component\Cache\ResettableInterface;
  16. use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
  17. use Symfony\Component\Cache\Traits\ContractsTrait;
  18. use Symfony\Contracts\Cache\CacheInterface;
  19. use Symfony\Contracts\Cache\NamespacedPoolInterface;
  20. /**
  21. * @author Nicolas Grekas <p@tchwork.com>
  22. */
  23. abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
  24. {
  25. use AbstractAdapterTrait;
  26. use ContractsTrait;
  27. /**
  28. * @internal
  29. */
  30. protected const NS_SEPARATOR = ':';
  31. private static bool $apcuSupported;
  32. protected function __construct(string $namespace = '', int $defaultLifetime = 0)
  33. {
  34. if ('' !== $namespace) {
  35. if (str_contains($namespace, static::NS_SEPARATOR)) {
  36. if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
  37. throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
  38. }
  39. CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
  40. } else {
  41. CacheItem::validateKey($namespace);
  42. }
  43. $this->namespace = $namespace.static::NS_SEPARATOR;
  44. }
  45. $this->rootNamespace = $this->namespace;
  46. $this->defaultLifetime = $defaultLifetime;
  47. if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
  48. throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
  49. }
  50. self::$createCacheItem ??= \Closure::bind(
  51. static function ($key, $value, $isHit) {
  52. $item = new CacheItem();
  53. $item->key = $key;
  54. $item->value = $value;
  55. $item->isHit = $isHit;
  56. $item->unpack();
  57. return $item;
  58. },
  59. null,
  60. CacheItem::class
  61. );
  62. self::$mergeByLifetime ??= \Closure::bind(
  63. static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
  64. $byLifetime = [];
  65. $now = microtime(true);
  66. $expiredIds = [];
  67. foreach ($deferred as $key => $item) {
  68. $key = (string) $key;
  69. if (null === $item->expiry) {
  70. $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
  71. } elseif (!$item->expiry) {
  72. $ttl = 0;
  73. } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
  74. $expiredIds[] = $getId($key);
  75. continue;
  76. }
  77. $byLifetime[$ttl][$getId($key)] = $item->pack();
  78. }
  79. return $byLifetime;
  80. },
  81. null,
  82. CacheItem::class
  83. );
  84. }
  85. /**
  86. * Returns the best possible adapter that your runtime supports.
  87. *
  88. * Using ApcuAdapter makes system caches compatible with read-only filesystems.
  89. */
  90. public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, ?LoggerInterface $logger = null): AdapterInterface
  91. {
  92. $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
  93. if (null !== $logger) {
  94. $opcache->setLogger($logger);
  95. }
  96. if (!self::$apcuSupported ??= ApcuAdapter::isSupported()) {
  97. return $opcache;
  98. }
  99. if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
  100. return $opcache;
  101. }
  102. $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
  103. if (null !== $logger) {
  104. $apcu->setLogger($logger);
  105. }
  106. return new ChainAdapter([$apcu, $opcache]);
  107. }
  108. public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
  109. {
  110. if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:')) {
  111. return RedisAdapter::createConnection($dsn, $options);
  112. }
  113. if (str_starts_with($dsn, 'memcached:')) {
  114. return MemcachedAdapter::createConnection($dsn, $options);
  115. }
  116. if (str_starts_with($dsn, 'couchbase:')) {
  117. if (class_exists(\CouchbaseBucket::class) && CouchbaseBucketAdapter::isSupported()) {
  118. return CouchbaseBucketAdapter::createConnection($dsn, $options);
  119. }
  120. return CouchbaseCollectionAdapter::createConnection($dsn, $options);
  121. }
  122. if (preg_match('/^(mysql|oci|pgsql|sqlsrv|sqlite):/', $dsn)) {
  123. return PdoAdapter::createConnection($dsn, $options);
  124. }
  125. throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "valkey[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
  126. }
  127. public function commit(): bool
  128. {
  129. $ok = true;
  130. $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, $this->getId(...), $this->defaultLifetime);
  131. $retry = $this->deferred = [];
  132. if ($expiredIds) {
  133. try {
  134. $this->doDelete($expiredIds);
  135. } catch (\Exception $e) {
  136. $ok = false;
  137. CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
  138. }
  139. }
  140. foreach ($byLifetime as $lifetime => $values) {
  141. try {
  142. $e = $this->doSave($values, $lifetime);
  143. } catch (\Exception $e) {
  144. }
  145. if (true === $e || [] === $e) {
  146. continue;
  147. }
  148. if (\is_array($e) || 1 === \count($values)) {
  149. foreach (\is_array($e) ? $e : array_keys($values) as $id) {
  150. $ok = false;
  151. $v = $values[$id];
  152. $type = get_debug_type($v);
  153. $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
  154. CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
  155. }
  156. } else {
  157. foreach ($values as $id => $v) {
  158. $retry[$lifetime][] = $id;
  159. }
  160. }
  161. }
  162. // When bulk-save failed, retry each item individually
  163. foreach ($retry as $lifetime => $ids) {
  164. foreach ($ids as $id) {
  165. try {
  166. $v = $byLifetime[$lifetime][$id];
  167. $e = $this->doSave([$id => $v], $lifetime);
  168. } catch (\Exception $e) {
  169. }
  170. if (true === $e || [] === $e) {
  171. continue;
  172. }
  173. $ok = false;
  174. $type = get_debug_type($v);
  175. $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
  176. CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
  177. }
  178. }
  179. return $ok;
  180. }
  181. }