AbstractAdapter.php 9.4 KB

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