AbstractAdapter.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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\Cache\CacheItemInterface;
  12. use Psr\Log\LoggerAwareInterface;
  13. use Psr\Log\LoggerInterface;
  14. use Psr\Log\NullLogger;
  15. use Symfony\Component\Cache\CacheItem;
  16. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  17. use Symfony\Component\Cache\ResettableInterface;
  18. use Symfony\Component\Cache\Traits\AbstractTrait;
  19. /**
  20. * @author Nicolas Grekas <p@tchwork.com>
  21. */
  22. abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface, ResettableInterface
  23. {
  24. use AbstractTrait;
  25. private static $apcuSupported;
  26. private static $phpFilesSupported;
  27. private $createCacheItem;
  28. private $mergeByLifetime;
  29. protected function __construct(string $namespace = '', int $defaultLifetime = 0)
  30. {
  31. $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
  32. if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
  33. throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, \strlen($namespace), $namespace));
  34. }
  35. $this->createCacheItem = \Closure::bind(
  36. function ($key, $value, $isHit) use ($defaultLifetime) {
  37. $item = new CacheItem();
  38. $item->key = $key;
  39. $item->value = $value;
  40. $item->isHit = $isHit;
  41. $item->defaultLifetime = $defaultLifetime;
  42. return $item;
  43. },
  44. null,
  45. CacheItem::class
  46. );
  47. $getId = \Closure::fromCallable([$this, 'getId']);
  48. $this->mergeByLifetime = \Closure::bind(
  49. function ($deferred, $namespace, &$expiredIds) use ($getId) {
  50. $byLifetime = [];
  51. $now = time();
  52. $expiredIds = [];
  53. foreach ($deferred as $key => $item) {
  54. $key = (string) $key;
  55. if (null === $item->expiry) {
  56. $byLifetime[0 < $item->defaultLifetime ? $item->defaultLifetime : 0][$getId($key)] = $item->value;
  57. } elseif ($item->expiry > $now) {
  58. $byLifetime[$item->expiry - $now][$getId($key)] = $item->value;
  59. } else {
  60. $expiredIds[] = $getId($key);
  61. }
  62. }
  63. return $byLifetime;
  64. },
  65. null,
  66. CacheItem::class
  67. );
  68. }
  69. /**
  70. * @param string $namespace
  71. * @param int $defaultLifetime
  72. * @param string $version
  73. * @param string $directory
  74. * @param LoggerInterface|null $logger
  75. *
  76. * @return AdapterInterface
  77. */
  78. public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null)
  79. {
  80. if (null === self::$apcuSupported) {
  81. self::$apcuSupported = ApcuAdapter::isSupported();
  82. }
  83. if (!self::$apcuSupported && null === self::$phpFilesSupported) {
  84. self::$phpFilesSupported = PhpFilesAdapter::isSupported();
  85. }
  86. if (self::$phpFilesSupported) {
  87. $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory);
  88. if (null !== $logger) {
  89. $opcache->setLogger($logger);
  90. }
  91. return $opcache;
  92. }
  93. $fs = new FilesystemAdapter($namespace, $defaultLifetime, $directory);
  94. if (null !== $logger) {
  95. $fs->setLogger($logger);
  96. }
  97. if (!self::$apcuSupported) {
  98. return $fs;
  99. }
  100. $apcu = new ApcuAdapter($namespace, (int) $defaultLifetime / 5, $version);
  101. if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) {
  102. $apcu->setLogger(new NullLogger());
  103. } elseif (null !== $logger) {
  104. $apcu->setLogger($logger);
  105. }
  106. return new ChainAdapter([$apcu, $fs]);
  107. }
  108. public static function createConnection($dsn, array $options = [])
  109. {
  110. if (!\is_string($dsn)) {
  111. throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, \gettype($dsn)));
  112. }
  113. if (0 === strpos($dsn, 'redis://')) {
  114. return RedisAdapter::createConnection($dsn, $options);
  115. }
  116. if (0 === strpos($dsn, 'memcached://')) {
  117. return MemcachedAdapter::createConnection($dsn, $options);
  118. }
  119. throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
  120. }
  121. /**
  122. * {@inheritdoc}
  123. */
  124. public function getItem($key)
  125. {
  126. if ($this->deferred) {
  127. $this->commit();
  128. }
  129. $id = $this->getId($key);
  130. $f = $this->createCacheItem;
  131. $isHit = false;
  132. $value = null;
  133. try {
  134. foreach ($this->doFetch([$id]) as $value) {
  135. $isHit = true;
  136. }
  137. } catch (\Exception $e) {
  138. CacheItem::log($this->logger, 'Failed to fetch key "{key}"', ['key' => $key, 'exception' => $e]);
  139. }
  140. return $f($key, $value, $isHit);
  141. }
  142. /**
  143. * {@inheritdoc}
  144. */
  145. public function getItems(array $keys = [])
  146. {
  147. if ($this->deferred) {
  148. $this->commit();
  149. }
  150. $ids = [];
  151. foreach ($keys as $key) {
  152. $ids[] = $this->getId($key);
  153. }
  154. try {
  155. $items = $this->doFetch($ids);
  156. } catch (\Exception $e) {
  157. CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => $keys, 'exception' => $e]);
  158. $items = [];
  159. }
  160. $ids = array_combine($ids, $keys);
  161. return $this->generateItems($items, $ids);
  162. }
  163. /**
  164. * {@inheritdoc}
  165. */
  166. public function save(CacheItemInterface $item)
  167. {
  168. if (!$item instanceof CacheItem) {
  169. return false;
  170. }
  171. $this->deferred[$item->getKey()] = $item;
  172. return $this->commit();
  173. }
  174. /**
  175. * {@inheritdoc}
  176. */
  177. public function saveDeferred(CacheItemInterface $item)
  178. {
  179. if (!$item instanceof CacheItem) {
  180. return false;
  181. }
  182. $this->deferred[$item->getKey()] = $item;
  183. return true;
  184. }
  185. /**
  186. * {@inheritdoc}
  187. */
  188. public function commit()
  189. {
  190. $ok = true;
  191. $byLifetime = $this->mergeByLifetime;
  192. $byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
  193. $retry = $this->deferred = [];
  194. if ($expiredIds) {
  195. $this->doDelete($expiredIds);
  196. }
  197. foreach ($byLifetime as $lifetime => $values) {
  198. try {
  199. $e = $this->doSave($values, $lifetime);
  200. } catch (\Exception $e) {
  201. }
  202. if (true === $e || [] === $e) {
  203. continue;
  204. }
  205. if (\is_array($e) || 1 === \count($values)) {
  206. foreach (\is_array($e) ? $e : array_keys($values) as $id) {
  207. $ok = false;
  208. $v = $values[$id];
  209. $type = \is_object($v) ? \get_class($v) : \gettype($v);
  210. CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
  211. }
  212. } else {
  213. foreach ($values as $id => $v) {
  214. $retry[$lifetime][] = $id;
  215. }
  216. }
  217. }
  218. // When bulk-save failed, retry each item individually
  219. foreach ($retry as $lifetime => $ids) {
  220. foreach ($ids as $id) {
  221. try {
  222. $v = $byLifetime[$lifetime][$id];
  223. $e = $this->doSave([$id => $v], $lifetime);
  224. } catch (\Exception $e) {
  225. }
  226. if (true === $e || [] === $e) {
  227. continue;
  228. }
  229. $ok = false;
  230. $type = \is_object($v) ? \get_class($v) : \gettype($v);
  231. CacheItem::log($this->logger, 'Failed to save key "{key}" ({type})', ['key' => substr($id, \strlen($this->namespace)), 'type' => $type, 'exception' => $e instanceof \Exception ? $e : null]);
  232. }
  233. }
  234. return $ok;
  235. }
  236. public function __destruct()
  237. {
  238. if ($this->deferred) {
  239. $this->commit();
  240. }
  241. }
  242. private function generateItems($items, &$keys)
  243. {
  244. $f = $this->createCacheItem;
  245. try {
  246. foreach ($items as $id => $value) {
  247. if (!isset($keys[$id])) {
  248. $id = key($keys);
  249. }
  250. $key = $keys[$id];
  251. unset($keys[$id]);
  252. yield $key => $f($key, $value, true);
  253. }
  254. } catch (\Exception $e) {
  255. CacheItem::log($this->logger, 'Failed to fetch requested items', ['keys' => array_values($keys), 'exception' => $e]);
  256. }
  257. foreach ($keys as $key) {
  258. yield $key => $f($key, null, false);
  259. }
  260. }
  261. }