AbstractAdapter.php 9.5 KB

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