├── Adapter ├── AbstractAdapter.php ├── AbstractTagAwareAdapter.php ├── AdapterInterface.php ├── ApcuAdapter.php ├── ArrayAdapter.php ├── ChainAdapter.php ├── CouchbaseBucketAdapter.php ├── CouchbaseCollectionAdapter.php ├── DoctrineDbalAdapter.php ├── FilesystemAdapter.php ├── FilesystemTagAwareAdapter.php ├── MemcachedAdapter.php ├── NullAdapter.php ├── ParameterNormalizer.php ├── PdoAdapter.php ├── PhpArrayAdapter.php ├── PhpFilesAdapter.php ├── ProxyAdapter.php ├── Psr16Adapter.php ├── RedisAdapter.php ├── RedisTagAwareAdapter.php ├── TagAwareAdapter.php ├── TagAwareAdapterInterface.php ├── TraceableAdapter.php └── TraceableTagAwareAdapter.php ├── CHANGELOG.md ├── CacheItem.php ├── DataCollector └── CacheDataCollector.php ├── DependencyInjection ├── CacheCollectorPass.php ├── CachePoolClearerPass.php ├── CachePoolPass.php └── CachePoolPrunerPass.php ├── Exception ├── BadMethodCallException.php ├── CacheException.php ├── InvalidArgumentException.php └── LogicException.php ├── LICENSE ├── LockRegistry.php ├── Marshaller ├── DefaultMarshaller.php ├── DeflateMarshaller.php ├── MarshallerInterface.php ├── SodiumMarshaller.php └── TagAwareMarshaller.php ├── Messenger ├── EarlyExpirationDispatcher.php ├── EarlyExpirationHandler.php └── EarlyExpirationMessage.php ├── PruneableInterface.php ├── Psr16Cache.php ├── README.md ├── ResettableInterface.php ├── Traits ├── AbstractAdapterTrait.php ├── ContractsTrait.php ├── FilesystemCommonTrait.php ├── FilesystemTrait.php ├── ProxyTrait.php ├── Redis5Proxy.php ├── Redis6Proxy.php ├── Redis6ProxyTrait.php ├── RedisCluster5Proxy.php ├── RedisCluster6Proxy.php ├── RedisCluster6ProxyTrait.php ├── RedisClusterNodeProxy.php ├── RedisClusterProxy.php ├── RedisProxy.php ├── RedisProxyTrait.php ├── RedisTrait.php ├── Relay │ ├── CopyTrait.php │ ├── GeosearchTrait.php │ ├── GetrangeTrait.php │ ├── HsetTrait.php │ ├── MoveTrait.php │ ├── NullableReturnTrait.php │ └── PfcountTrait.php ├── RelayClusterProxy.php ├── RelayProxy.php ├── RelayProxyTrait.php └── ValueWrapper.php └── composer.json /Adapter/AbstractAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Cache\Adapter; 13 | 14 | use Psr\Log\LoggerAwareInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Cache\CacheItem; 17 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 18 | use Symfony\Component\Cache\ResettableInterface; 19 | use Symfony\Component\Cache\Traits\AbstractAdapterTrait; 20 | use Symfony\Component\Cache\Traits\ContractsTrait; 21 | use Symfony\Contracts\Cache\CacheInterface; 22 | use Symfony\Contracts\Cache\NamespacedPoolInterface; 23 | 24 | /** 25 | * @author Nicolas Grekas
26 | */
27 | abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface
28 | {
29 | use AbstractAdapterTrait;
30 | use ContractsTrait;
31 |
32 | /**
33 | * @internal
34 | */
35 | protected const NS_SEPARATOR = ':';
36 |
37 | private static bool $apcuSupported;
38 |
39 | protected function __construct(string $namespace = '', int $defaultLifetime = 0)
40 | {
41 | if ('' !== $namespace) {
42 | if (str_contains($namespace, static::NS_SEPARATOR)) {
43 | if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) {
44 | throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace));
45 | }
46 | CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace));
47 | } else {
48 | CacheItem::validateKey($namespace);
49 | }
50 | $this->namespace = $namespace.static::NS_SEPARATOR;
51 | }
52 | $this->rootNamespace = $this->namespace;
53 |
54 | $this->defaultLifetime = $defaultLifetime;
55 | if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
56 | throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
57 | }
58 | self::$createCacheItem ??= \Closure::bind(
59 | static function ($key, $value, $isHit) {
60 | $item = new CacheItem();
61 | $item->key = $key;
62 | $item->value = $value;
63 | $item->isHit = $isHit;
64 | $item->unpack();
65 |
66 | return $item;
67 | },
68 | null,
69 | CacheItem::class
70 | );
71 | self::$mergeByLifetime ??= \Closure::bind(
72 | static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
73 | $byLifetime = [];
74 | $now = microtime(true);
75 | $expiredIds = [];
76 |
77 | foreach ($deferred as $key => $item) {
78 | $key = (string) $key;
79 | if (null === $item->expiry) {
80 | $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
81 | } elseif (!$item->expiry) {
82 | $ttl = 0;
83 | } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
84 | $expiredIds[] = $getId($key);
85 | continue;
86 | }
87 | $byLifetime[$ttl][$getId($key)] = $item->pack();
88 | }
89 |
90 | return $byLifetime;
91 | },
92 | null,
93 | CacheItem::class
94 | );
95 | }
96 |
97 | /**
98 | * Returns the best possible adapter that your runtime supports.
99 | *
100 | * Using ApcuAdapter makes system caches compatible with read-only filesystems.
101 | */
102 | public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, ?LoggerInterface $logger = null): AdapterInterface
103 | {
104 | $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
105 | if (null !== $logger) {
106 | $opcache->setLogger($logger);
107 | }
108 |
109 | if (!self::$apcuSupported ??= ApcuAdapter::isSupported()) {
110 | return $opcache;
111 | }
112 |
113 | if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
114 | return $opcache;
115 | }
116 |
117 | $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
118 | if (null !== $logger) {
119 | $apcu->setLogger($logger);
120 | }
121 |
122 | return new ChainAdapter([$apcu, $opcache]);
123 | }
124 |
125 | public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
126 | {
127 | if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:')) {
128 | return RedisAdapter::createConnection($dsn, $options);
129 | }
130 | if (str_starts_with($dsn, 'memcached:')) {
131 | return MemcachedAdapter::createConnection($dsn, $options);
132 | }
133 | if (str_starts_with($dsn, 'couchbase:')) {
134 | if (class_exists(\CouchbaseBucket::class) && CouchbaseBucketAdapter::isSupported()) {
135 | return CouchbaseBucketAdapter::createConnection($dsn, $options);
136 | }
137 |
138 | return CouchbaseCollectionAdapter::createConnection($dsn, $options);
139 | }
140 | if (preg_match('/^(mysql|oci|pgsql|sqlsrv|sqlite):/', $dsn)) {
141 | return PdoAdapter::createConnection($dsn, $options);
142 | }
143 |
144 | throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "valkey[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
145 | }
146 |
147 | public function commit(): bool
148 | {
149 | $ok = true;
150 | $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, $this->getId(...), $this->defaultLifetime);
151 | $retry = $this->deferred = [];
152 |
153 | if ($expiredIds) {
154 | try {
155 | $this->doDelete($expiredIds);
156 | } catch (\Exception $e) {
157 | $ok = false;
158 | CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
159 | }
160 | }
161 | foreach ($byLifetime as $lifetime => $values) {
162 | try {
163 | $e = $this->doSave($values, $lifetime);
164 | } catch (\Exception $e) {
165 | }
166 | if (true === $e || [] === $e) {
167 | continue;
168 | }
169 | if (\is_array($e) || 1 === \count($values)) {
170 | foreach (\is_array($e) ? $e : array_keys($values) as $id) {
171 | $ok = false;
172 | $v = $values[$id];
173 | $type = get_debug_type($v);
174 | $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
175 | CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
176 | }
177 | } else {
178 | foreach ($values as $id => $v) {
179 | $retry[$lifetime][] = $id;
180 | }
181 | }
182 | }
183 |
184 | // When bulk-save failed, retry each item individually
185 | foreach ($retry as $lifetime => $ids) {
186 | foreach ($ids as $id) {
187 | try {
188 | $v = $byLifetime[$lifetime][$id];
189 | $e = $this->doSave([$id => $v], $lifetime);
190 | } catch (\Exception $e) {
191 | }
192 | if (true === $e || [] === $e) {
193 | continue;
194 | }
195 | $ok = false;
196 | $type = get_debug_type($v);
197 | $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
198 | CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
199 | }
200 | }
201 |
202 | return $ok;
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/Adapter/AdapterInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Psr\Cache\CacheItemPoolInterface;
15 | use Symfony\Component\Cache\CacheItem;
16 |
17 | // Help opcache.preload discover always-needed symbols
18 | class_exists(CacheItem::class);
19 |
20 | /**
21 | * Interface for adapters managing instances of Symfony's CacheItem.
22 | *
23 | * @author Kévin Dunglas
20 | */
21 | class ApcuAdapter extends AbstractAdapter
22 | {
23 | /**
24 | * @throws CacheException if APCu is not enabled
25 | */
26 | public function __construct(
27 | string $namespace = '',
28 | int $defaultLifetime = 0,
29 | ?string $version = null,
30 | private ?MarshallerInterface $marshaller = null,
31 | ) {
32 | if (!static::isSupported()) {
33 | throw new CacheException('APCu is not enabled.');
34 | }
35 | if ('cli' === \PHP_SAPI) {
36 | ini_set('apc.use_request_time', 0);
37 | }
38 | parent::__construct($namespace, $defaultLifetime);
39 |
40 | if (null !== $version) {
41 | CacheItem::validateKey($version);
42 |
43 | if (!apcu_exists($version.'@'.$namespace)) {
44 | $this->doClear($namespace);
45 | apcu_add($version.'@'.$namespace, null);
46 | }
47 | }
48 | }
49 |
50 | public static function isSupported(): bool
51 | {
52 | return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL);
53 | }
54 |
55 | protected function doFetch(array $ids): iterable
56 | {
57 | $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
58 | try {
59 | $values = [];
60 | foreach (apcu_fetch($ids, $ok) ?: [] as $k => $v) {
61 | if (null !== $v || $ok) {
62 | $values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v;
63 | }
64 | }
65 |
66 | return $values;
67 | } catch (\Error $e) {
68 | throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
69 | } finally {
70 | ini_set('unserialize_callback_func', $unserializeCallbackHandler);
71 | }
72 | }
73 |
74 | protected function doHave(string $id): bool
75 | {
76 | return apcu_exists($id);
77 | }
78 |
79 | protected function doClear(string $namespace): bool
80 | {
81 | return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL))
82 | ? apcu_delete(new \APCUIterator(\sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY))
83 | : apcu_clear_cache();
84 | }
85 |
86 | protected function doDelete(array $ids): bool
87 | {
88 | foreach ($ids as $id) {
89 | apcu_delete($id);
90 | }
91 |
92 | return true;
93 | }
94 |
95 | protected function doSave(array $values, int $lifetime): array|bool
96 | {
97 | if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) {
98 | return $failed;
99 | }
100 |
101 | if (false === $failures = apcu_store($values, null, $lifetime)) {
102 | $failures = $values;
103 | }
104 |
105 | return array_keys($failures);
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Adapter/ChainAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Psr\Cache\CacheItemInterface;
15 | use Psr\Cache\CacheItemPoolInterface;
16 | use Symfony\Component\Cache\CacheItem;
17 | use Symfony\Component\Cache\Exception\BadMethodCallException;
18 | use Symfony\Component\Cache\Exception\InvalidArgumentException;
19 | use Symfony\Component\Cache\PruneableInterface;
20 | use Symfony\Component\Cache\ResettableInterface;
21 | use Symfony\Component\Cache\Traits\ContractsTrait;
22 | use Symfony\Contracts\Cache\CacheInterface;
23 | use Symfony\Contracts\Cache\NamespacedPoolInterface;
24 | use Symfony\Contracts\Service\ResetInterface;
25 |
26 | /**
27 | * Chains several adapters together.
28 | *
29 | * Cached items are fetched from the first adapter having them in its data store.
30 | * They are saved and deleted in all adapters at once.
31 | *
32 | * @author Kévin Dunglas
23 | * @author André Rømcke
23 | * @author Rob Frawley 2nd
26 | */
27 | class ProxyAdapter implements AdapterInterface, NamespacedPoolInterface, CacheInterface, PruneableInterface, ResettableInterface
28 | {
29 | use ContractsTrait;
30 | use ProxyTrait;
31 |
32 | private string $namespace = '';
33 | private int $namespaceLen;
34 | private string $poolHash;
35 | private int $defaultLifetime;
36 |
37 | private static \Closure $createCacheItem;
38 | private static \Closure $setInnerItem;
39 |
40 | public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
41 | {
42 | if ('' !== $namespace) {
43 | if ($pool instanceof NamespacedPoolInterface) {
44 | $pool = $pool->withSubNamespace($namespace);
45 | $this->namespace = $namespace = '';
46 | } else {
47 | \assert('' !== CacheItem::validateKey($namespace));
48 | $this->namespace = $namespace;
49 | }
50 | }
51 | $this->pool = $pool;
52 | $this->poolHash = spl_object_hash($pool);
53 | $this->namespaceLen = \strlen($namespace);
54 | $this->defaultLifetime = $defaultLifetime;
55 | self::$createCacheItem ??= \Closure::bind(
56 | static function ($key, $innerItem, $poolHash) {
57 | $item = new CacheItem();
58 | $item->key = $key;
59 |
60 | if (null === $innerItem) {
61 | return $item;
62 | }
63 |
64 | $item->value = $innerItem->get();
65 | $item->isHit = $innerItem->isHit();
66 | $item->innerItem = $innerItem;
67 | $item->poolHash = $poolHash;
68 |
69 | if (!$item->unpack() && $innerItem instanceof CacheItem) {
70 | $item->metadata = $innerItem->metadata;
71 | }
72 | $innerItem->set(null);
73 |
74 | return $item;
75 | },
76 | null,
77 | CacheItem::class
78 | );
79 | self::$setInnerItem ??= \Closure::bind(
80 | static function (CacheItemInterface $innerItem, CacheItem $item, $expiry = null) {
81 | $innerItem->set($item->pack());
82 | $innerItem->expiresAt(($expiry ?? $item->expiry) ? \DateTimeImmutable::createFromFormat('U.u', \sprintf('%.6F', $expiry ?? $item->expiry)) : null);
83 | },
84 | null,
85 | CacheItem::class
86 | );
87 | }
88 |
89 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
90 | {
91 | if (!$this->pool instanceof CacheInterface) {
92 | return $this->doGet($this, $key, $callback, $beta, $metadata);
93 | }
94 |
95 | return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) {
96 | $item = (self::$createCacheItem)($key, $innerItem, $this->poolHash);
97 | $item->set($value = $callback($item, $save));
98 | (self::$setInnerItem)($innerItem, $item);
99 |
100 | return $value;
101 | }, $beta, $metadata);
102 | }
103 |
104 | public function getItem(mixed $key): CacheItem
105 | {
106 | $item = $this->pool->getItem($this->getId($key));
107 |
108 | return (self::$createCacheItem)($key, $item, $this->poolHash);
109 | }
110 |
111 | public function getItems(array $keys = []): iterable
112 | {
113 | if ($this->namespaceLen) {
114 | foreach ($keys as $i => $key) {
115 | $keys[$i] = $this->getId($key);
116 | }
117 | }
118 |
119 | return $this->generateItems($this->pool->getItems($keys));
120 | }
121 |
122 | public function hasItem(mixed $key): bool
123 | {
124 | return $this->pool->hasItem($this->getId($key));
125 | }
126 |
127 | public function clear(string $prefix = ''): bool
128 | {
129 | if ($this->pool instanceof AdapterInterface) {
130 | return $this->pool->clear($this->namespace.$prefix);
131 | }
132 |
133 | return $this->pool->clear();
134 | }
135 |
136 | public function deleteItem(mixed $key): bool
137 | {
138 | return $this->pool->deleteItem($this->getId($key));
139 | }
140 |
141 | public function deleteItems(array $keys): bool
142 | {
143 | if ($this->namespaceLen) {
144 | foreach ($keys as $i => $key) {
145 | $keys[$i] = $this->getId($key);
146 | }
147 | }
148 |
149 | return $this->pool->deleteItems($keys);
150 | }
151 |
152 | public function save(CacheItemInterface $item): bool
153 | {
154 | return $this->doSave($item, __FUNCTION__);
155 | }
156 |
157 | public function saveDeferred(CacheItemInterface $item): bool
158 | {
159 | return $this->doSave($item, __FUNCTION__);
160 | }
161 |
162 | public function commit(): bool
163 | {
164 | return $this->pool->commit();
165 | }
166 |
167 | public function withSubNamespace(string $namespace): static
168 | {
169 | $clone = clone $this;
170 |
171 | if ($clone->pool instanceof NamespacedPoolInterface) {
172 | $clone->pool = $clone->pool->withSubNamespace($namespace);
173 | } else {
174 | $clone->namespace .= CacheItem::validateKey($namespace);
175 | $clone->namespaceLen = \strlen($clone->namespace);
176 | }
177 |
178 | return $clone;
179 | }
180 |
181 | private function doSave(CacheItemInterface $item, string $method): bool
182 | {
183 | if (!$item instanceof CacheItem) {
184 | return false;
185 | }
186 | $castItem = (array) $item;
187 |
188 | if (null === $castItem["\0*\0expiry"] && 0 < $this->defaultLifetime) {
189 | $castItem["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
190 | }
191 |
192 | if ($castItem["\0*\0poolHash"] === $this->poolHash && $castItem["\0*\0innerItem"]) {
193 | $innerItem = $castItem["\0*\0innerItem"];
194 | } elseif ($this->pool instanceof AdapterInterface) {
195 | // this is an optimization specific for AdapterInterface implementations
196 | // so we can save a round-trip to the backend by just creating a new item
197 | $innerItem = (self::$createCacheItem)($this->namespace.$castItem["\0*\0key"], null, $this->poolHash);
198 | } else {
199 | $innerItem = $this->pool->getItem($this->namespace.$castItem["\0*\0key"]);
200 | }
201 |
202 | (self::$setInnerItem)($innerItem, $item, $castItem["\0*\0expiry"]);
203 |
204 | return $this->pool->$method($innerItem);
205 | }
206 |
207 | private function generateItems(iterable $items): \Generator
208 | {
209 | $f = self::$createCacheItem;
210 |
211 | foreach ($items as $key => $item) {
212 | if ($this->namespaceLen) {
213 | $key = substr($key, $this->namespaceLen);
214 | }
215 |
216 | yield $key => $f($key, $item, $this->poolHash);
217 | }
218 | }
219 |
220 | private function getId(mixed $key): string
221 | {
222 | \assert('' !== CacheItem::validateKey($key));
223 |
224 | return $this->namespace.$key;
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/Adapter/Psr16Adapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Psr\SimpleCache\CacheInterface;
15 | use Symfony\Component\Cache\PruneableInterface;
16 | use Symfony\Component\Cache\ResettableInterface;
17 | use Symfony\Component\Cache\Traits\ProxyTrait;
18 |
19 | /**
20 | * Turns a PSR-16 cache into a PSR-6 one.
21 | *
22 | * @author Nicolas Grekas
23 | */
24 | class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface
25 | {
26 | use ProxyTrait;
27 |
28 | /**
29 | * @internal
30 | */
31 | protected const NS_SEPARATOR = '_';
32 |
33 | private object $miss;
34 |
35 | public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0)
36 | {
37 | parent::__construct($namespace, $defaultLifetime);
38 |
39 | $this->pool = $pool;
40 | $this->miss = new \stdClass();
41 | }
42 |
43 | protected function doFetch(array $ids): iterable
44 | {
45 | foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) {
46 | if ($this->miss !== $value) {
47 | yield $key => $value;
48 | }
49 | }
50 | }
51 |
52 | protected function doHave(string $id): bool
53 | {
54 | return $this->pool->has($id);
55 | }
56 |
57 | protected function doClear(string $namespace): bool
58 | {
59 | return $this->pool->clear();
60 | }
61 |
62 | protected function doDelete(array $ids): bool
63 | {
64 | return $this->pool->deleteMultiple($ids);
65 | }
66 |
67 | protected function doSave(array $values, int $lifetime): array|bool
68 | {
69 | return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Adapter/RedisAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface;
15 | use Symfony\Component\Cache\Traits\RedisTrait;
16 |
17 | class RedisAdapter extends AbstractAdapter
18 | {
19 | use RedisTrait;
20 |
21 | public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay|\Relay\Cluster $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
22 | {
23 | $this->init($redis, $namespace, $defaultLifetime, $marshaller);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Adapter/RedisTagAwareAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Predis\Connection\Aggregate\ClusterInterface;
15 | use Predis\Connection\Aggregate\PredisCluster;
16 | use Predis\Connection\Aggregate\ReplicationInterface;
17 | use Predis\Response\ErrorInterface;
18 | use Predis\Response\Status;
19 | use Relay\Relay;
20 | use Symfony\Component\Cache\CacheItem;
21 | use Symfony\Component\Cache\Exception\InvalidArgumentException;
22 | use Symfony\Component\Cache\Exception\LogicException;
23 | use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
24 | use Symfony\Component\Cache\Marshaller\MarshallerInterface;
25 | use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
26 | use Symfony\Component\Cache\Traits\RedisTrait;
27 |
28 | /**
29 | * Stores tag id <> cache id relationship as a Redis Set.
30 | *
31 | * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even
32 | * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache
33 | * relationship survives eviction (cache cleanup when Redis runs out of memory).
34 | *
35 | * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up
36 | *
37 | * Design limitations:
38 | * - Max 4 billion cache keys per cache tag as limited by Redis Set datatype.
39 | * E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also.
40 | *
41 | * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies.
42 | * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype.
43 | *
44 | * @author Nicolas Grekas
45 | * @author André Rømcke
20 | */
21 | interface TagAwareAdapterInterface extends AdapterInterface
22 | {
23 | /**
24 | * Invalidates cached items using tags.
25 | *
26 | * @param string[] $tags An array of tags to invalidate
27 | *
28 | * @throws InvalidArgumentException When $tags is not valid
29 | */
30 | public function invalidateTags(array $tags): bool;
31 | }
32 |
--------------------------------------------------------------------------------
/Adapter/TraceableAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Psr\Cache\CacheItemInterface;
15 | use Symfony\Component\Cache\CacheItem;
16 | use Symfony\Component\Cache\Exception\BadMethodCallException;
17 | use Symfony\Component\Cache\PruneableInterface;
18 | use Symfony\Component\Cache\ResettableInterface;
19 | use Symfony\Contracts\Cache\CacheInterface;
20 | use Symfony\Contracts\Cache\NamespacedPoolInterface;
21 | use Symfony\Contracts\Service\ResetInterface;
22 |
23 | /**
24 | * An adapter that collects data about all cache calls.
25 | *
26 | * @author Aaron Scherer
29 | */
30 | class TraceableAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface
31 | {
32 | private string $namespace = '';
33 | private array $calls = [];
34 |
35 | public function __construct(
36 | protected AdapterInterface $pool,
37 | protected readonly ?\Closure $disabled = null,
38 | ) {
39 | }
40 |
41 | /**
42 | * @throws BadMethodCallException When the item pool is not a CacheInterface
43 | */
44 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
45 | {
46 | if (!$this->pool instanceof CacheInterface) {
47 | throw new BadMethodCallException(\sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
48 | }
49 | if ($this->disabled?->__invoke()) {
50 | return $this->pool->get($key, $callback, $beta, $metadata);
51 | }
52 |
53 | $isHit = true;
54 | $callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) {
55 | $isHit = $item->isHit();
56 |
57 | return $callback($item, $save);
58 | };
59 |
60 | $event = $this->start(__FUNCTION__);
61 | try {
62 | $value = $this->pool->get($key, $callback, $beta, $metadata);
63 | $event->result[$key] = get_debug_type($value);
64 | } finally {
65 | $event->end = microtime(true);
66 | }
67 | if ($isHit) {
68 | ++$event->hits;
69 | } else {
70 | ++$event->misses;
71 | }
72 |
73 | return $value;
74 | }
75 |
76 | public function getItem(mixed $key): CacheItem
77 | {
78 | if ($this->disabled?->__invoke()) {
79 | return $this->pool->getItem($key);
80 | }
81 | $event = $this->start(__FUNCTION__);
82 | try {
83 | $item = $this->pool->getItem($key);
84 | } finally {
85 | $event->end = microtime(true);
86 | }
87 | if ($event->result[$key] = $item->isHit()) {
88 | ++$event->hits;
89 | } else {
90 | ++$event->misses;
91 | }
92 |
93 | return $item;
94 | }
95 |
96 | public function hasItem(mixed $key): bool
97 | {
98 | if ($this->disabled?->__invoke()) {
99 | return $this->pool->hasItem($key);
100 | }
101 | $event = $this->start(__FUNCTION__);
102 | try {
103 | return $event->result[$key] = $this->pool->hasItem($key);
104 | } finally {
105 | $event->end = microtime(true);
106 | }
107 | }
108 |
109 | public function deleteItem(mixed $key): bool
110 | {
111 | if ($this->disabled?->__invoke()) {
112 | return $this->pool->deleteItem($key);
113 | }
114 | $event = $this->start(__FUNCTION__);
115 | try {
116 | return $event->result[$key] = $this->pool->deleteItem($key);
117 | } finally {
118 | $event->end = microtime(true);
119 | }
120 | }
121 |
122 | public function save(CacheItemInterface $item): bool
123 | {
124 | if ($this->disabled?->__invoke()) {
125 | return $this->pool->save($item);
126 | }
127 | $event = $this->start(__FUNCTION__);
128 | try {
129 | return $event->result[$item->getKey()] = $this->pool->save($item);
130 | } finally {
131 | $event->end = microtime(true);
132 | }
133 | }
134 |
135 | public function saveDeferred(CacheItemInterface $item): bool
136 | {
137 | if ($this->disabled?->__invoke()) {
138 | return $this->pool->saveDeferred($item);
139 | }
140 | $event = $this->start(__FUNCTION__);
141 | try {
142 | return $event->result[$item->getKey()] = $this->pool->saveDeferred($item);
143 | } finally {
144 | $event->end = microtime(true);
145 | }
146 | }
147 |
148 | public function getItems(array $keys = []): iterable
149 | {
150 | if ($this->disabled?->__invoke()) {
151 | return $this->pool->getItems($keys);
152 | }
153 | $event = $this->start(__FUNCTION__);
154 | try {
155 | $result = $this->pool->getItems($keys);
156 | } finally {
157 | $event->end = microtime(true);
158 | }
159 | $f = function () use ($result, $event) {
160 | $event->result = [];
161 | foreach ($result as $key => $item) {
162 | if ($event->result[$key] = $item->isHit()) {
163 | ++$event->hits;
164 | } else {
165 | ++$event->misses;
166 | }
167 | yield $key => $item;
168 | }
169 | };
170 |
171 | return $f();
172 | }
173 |
174 | public function clear(string $prefix = ''): bool
175 | {
176 | if ($this->disabled?->__invoke()) {
177 | return $this->pool->clear($prefix);
178 | }
179 | $event = $this->start(__FUNCTION__);
180 | try {
181 | if ($this->pool instanceof AdapterInterface) {
182 | return $event->result = $this->pool->clear($prefix);
183 | }
184 |
185 | return $event->result = $this->pool->clear();
186 | } finally {
187 | $event->end = microtime(true);
188 | }
189 | }
190 |
191 | public function deleteItems(array $keys): bool
192 | {
193 | if ($this->disabled?->__invoke()) {
194 | return $this->pool->deleteItems($keys);
195 | }
196 | $event = $this->start(__FUNCTION__);
197 | $event->result['keys'] = $keys;
198 | try {
199 | return $event->result['result'] = $this->pool->deleteItems($keys);
200 | } finally {
201 | $event->end = microtime(true);
202 | }
203 | }
204 |
205 | public function commit(): bool
206 | {
207 | if ($this->disabled?->__invoke()) {
208 | return $this->pool->commit();
209 | }
210 | $event = $this->start(__FUNCTION__);
211 | try {
212 | return $event->result = $this->pool->commit();
213 | } finally {
214 | $event->end = microtime(true);
215 | }
216 | }
217 |
218 | public function prune(): bool
219 | {
220 | if (!$this->pool instanceof PruneableInterface) {
221 | return false;
222 | }
223 | if ($this->disabled?->__invoke()) {
224 | return $this->pool->prune();
225 | }
226 | $event = $this->start(__FUNCTION__);
227 | try {
228 | return $event->result = $this->pool->prune();
229 | } finally {
230 | $event->end = microtime(true);
231 | }
232 | }
233 |
234 | public function reset(): void
235 | {
236 | if ($this->pool instanceof ResetInterface) {
237 | $this->pool->reset();
238 | }
239 |
240 | $this->clearCalls();
241 | }
242 |
243 | public function delete(string $key): bool
244 | {
245 | if ($this->disabled?->__invoke()) {
246 | return $this->pool->deleteItem($key);
247 | }
248 | $event = $this->start(__FUNCTION__);
249 | try {
250 | return $event->result[$key] = $this->pool->deleteItem($key);
251 | } finally {
252 | $event->end = microtime(true);
253 | }
254 | }
255 |
256 | public function getCalls(): array
257 | {
258 | return $this->calls;
259 | }
260 |
261 | public function clearCalls(): void
262 | {
263 | $this->calls = [];
264 | }
265 |
266 | public function getPool(): AdapterInterface
267 | {
268 | return $this->pool;
269 | }
270 |
271 | /**
272 | * @throws BadMethodCallException When the item pool is not a NamespacedPoolInterface
273 | */
274 | public function withSubNamespace(string $namespace): static
275 | {
276 | if (!$this->pool instanceof NamespacedPoolInterface) {
277 | throw new BadMethodCallException(\sprintf('Cannot call "%s::withSubNamespace()": this class doesn\'t implement "%s".', get_debug_type($this->pool), NamespacedPoolInterface::class));
278 | }
279 |
280 | $calls = &$this->calls; // ensures clones share the same array
281 | $clone = clone $this;
282 | $clone->namespace .= CacheItem::validateKey($namespace).':';
283 | $clone->pool = $this->pool->withSubNamespace($namespace);
284 |
285 | return $clone;
286 | }
287 |
288 | protected function start(string $name): TraceableAdapterEvent
289 | {
290 | $this->calls[] = $event = new TraceableAdapterEvent();
291 | $event->name = $name;
292 | $event->start = microtime(true);
293 | $event->namespace = $this->namespace;
294 |
295 | return $event;
296 | }
297 | }
298 |
299 | /**
300 | * @internal
301 | */
302 | class TraceableAdapterEvent
303 | {
304 | public string $name;
305 | public float $start;
306 | public float $end;
307 | public array|bool $result;
308 | public int $hits = 0;
309 | public int $misses = 0;
310 | public string $namespace;
311 | }
312 |
--------------------------------------------------------------------------------
/Adapter/TraceableTagAwareAdapter.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Adapter;
13 |
14 | use Symfony\Contracts\Cache\TagAwareCacheInterface;
15 |
16 | /**
17 | * @author Robin Chalas
22 | */
23 | final class CacheItem implements ItemInterface
24 | {
25 | private const METADATA_EXPIRY_OFFSET = 1527506807;
26 | private const VALUE_WRAPPER = "\xA9";
27 |
28 | protected string $key;
29 | protected mixed $value = null;
30 | protected bool $isHit = false;
31 | protected float|int|null $expiry = null;
32 | protected array $metadata = [];
33 | protected array $newMetadata = [];
34 | protected ?CacheItemInterface $innerItem = null;
35 | protected ?string $poolHash = null;
36 | protected bool $isTaggable = false;
37 |
38 | public function getKey(): string
39 | {
40 | return $this->key;
41 | }
42 |
43 | public function get(): mixed
44 | {
45 | return $this->value;
46 | }
47 |
48 | public function isHit(): bool
49 | {
50 | return $this->isHit;
51 | }
52 |
53 | /**
54 | * @return $this
55 | */
56 | public function set($value): static
57 | {
58 | $this->value = $value;
59 |
60 | return $this;
61 | }
62 |
63 | /**
64 | * @return $this
65 | */
66 | public function expiresAt(?\DateTimeInterface $expiration): static
67 | {
68 | $this->expiry = null !== $expiration ? (float) $expiration->format('U.u') : null;
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * @return $this
75 | */
76 | public function expiresAfter(mixed $time): static
77 | {
78 | if (null === $time) {
79 | $this->expiry = null;
80 | } elseif ($time instanceof \DateInterval) {
81 | $this->expiry = microtime(true) + \DateTimeImmutable::createFromFormat('U', 0)->add($time)->format('U.u');
82 | } elseif (\is_int($time)) {
83 | $this->expiry = $time + microtime(true);
84 | } else {
85 | throw new InvalidArgumentException(\sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time)));
86 | }
87 |
88 | return $this;
89 | }
90 |
91 | public function tag(mixed $tags): static
92 | {
93 | if (!$this->isTaggable) {
94 | throw new LogicException(\sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
95 | }
96 | if (!\is_array($tags) && !$tags instanceof \Traversable) { // don't use is_iterable(), it's slow
97 | $tags = [$tags];
98 | }
99 | foreach ($tags as $tag) {
100 | if (!\is_string($tag) && !$tag instanceof \Stringable) {
101 | throw new InvalidArgumentException(\sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', get_debug_type($tag)));
102 | }
103 | $tag = (string) $tag;
104 | if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
105 | continue;
106 | }
107 | if ('' === $tag) {
108 | throw new InvalidArgumentException('Cache tag length must be greater than zero.');
109 | }
110 | if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) {
111 | throw new InvalidArgumentException(\sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS));
112 | }
113 | $this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
114 | }
115 |
116 | return $this;
117 | }
118 |
119 | public function getMetadata(): array
120 | {
121 | return $this->metadata;
122 | }
123 |
124 | /**
125 | * Validates a cache key according to PSR-6.
126 | *
127 | * @param mixed $key The key to validate
128 | *
129 | * @throws InvalidArgumentException When $key is not valid
130 | */
131 | public static function validateKey($key): string
132 | {
133 | if (!\is_string($key)) {
134 | throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
135 | }
136 | if ('' === $key) {
137 | throw new InvalidArgumentException('Cache key length must be greater than zero.');
138 | }
139 | if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) {
140 | throw new InvalidArgumentException(\sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
141 | }
142 |
143 | return $key;
144 | }
145 |
146 | /**
147 | * Internal logging helper.
148 | *
149 | * @internal
150 | */
151 | public static function log(?LoggerInterface $logger, string $message, array $context = []): void
152 | {
153 | if ($logger) {
154 | $logger->warning($message, $context);
155 | } else {
156 | $replace = [];
157 | foreach ($context as $k => $v) {
158 | if (\is_scalar($v)) {
159 | $replace['{'.$k.'}'] = $v;
160 | }
161 | }
162 | @trigger_error(strtr($message, $replace), \E_USER_WARNING);
163 | }
164 | }
165 |
166 | private function pack(): mixed
167 | {
168 | if (!$m = $this->newMetadata) {
169 | return $this->value;
170 | }
171 | $valueWrapper = self::VALUE_WRAPPER;
172 |
173 | return new $valueWrapper($this->value, $m + ['expiry' => $this->expiry]);
174 | }
175 |
176 | private function unpack(): bool
177 | {
178 | $v = $this->value;
179 | $valueWrapper = self::VALUE_WRAPPER;
180 |
181 | if ($v instanceof $valueWrapper) {
182 | $this->value = $v->value;
183 | $this->metadata = $v->metadata;
184 |
185 | return true;
186 | }
187 |
188 | if (!\is_array($v) || 1 !== \count($v) || 10 !== \strlen($k = (string) array_key_first($v)) || "\x9D" !== $k[0] || "\0" !== $k[5] || "\x5F" !== $k[9]) {
189 | return false;
190 | }
191 |
192 | // BC with pools populated before v6.1
193 | $this->value = $v[$k];
194 | $this->metadata = unpack('Vexpiry/Nctime', substr($k, 1, -1));
195 | $this->metadata['expiry'] += self::METADATA_EXPIRY_OFFSET;
196 |
197 | return true;
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/DataCollector/CacheDataCollector.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\DataCollector;
13 |
14 | use Symfony\Component\Cache\Adapter\TraceableAdapter;
15 | use Symfony\Component\Cache\Adapter\TraceableAdapterEvent;
16 | use Symfony\Component\HttpFoundation\Request;
17 | use Symfony\Component\HttpFoundation\Response;
18 | use Symfony\Component\HttpKernel\DataCollector\DataCollector;
19 | use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
20 |
21 | /**
22 | * @author Aaron Scherer
20 | */
21 | class CachePoolClearerPass implements CompilerPassInterface
22 | {
23 | public function process(ContainerBuilder $container): void
24 | {
25 | $container->getParameterBag()->remove('cache.prefix.seed');
26 |
27 | foreach ($container->findTaggedServiceIds('cache.pool.clearer') as $id => $attr) {
28 | $clearer = $container->getDefinition($id);
29 | $pools = [];
30 | foreach ($clearer->getArgument(0) as $name => $ref) {
31 | if ($container->hasDefinition($ref)) {
32 | $pools[$name] = new Reference($ref);
33 | }
34 | }
35 | $clearer->replaceArgument(0, $pools);
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/DependencyInjection/CachePoolPass.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\DependencyInjection;
13 |
14 | use Symfony\Component\Cache\Adapter\AbstractAdapter;
15 | use Symfony\Component\Cache\Adapter\ArrayAdapter;
16 | use Symfony\Component\Cache\Adapter\ChainAdapter;
17 | use Symfony\Component\Cache\Adapter\NullAdapter;
18 | use Symfony\Component\Cache\Adapter\ParameterNormalizer;
19 | use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher;
20 | use Symfony\Component\DependencyInjection\ChildDefinition;
21 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
22 | use Symfony\Component\DependencyInjection\ContainerBuilder;
23 | use Symfony\Component\DependencyInjection\Definition;
24 | use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
25 | use Symfony\Component\DependencyInjection\Reference;
26 |
27 | /**
28 | * @author Nicolas Grekas
29 | */
30 | class CachePoolPass implements CompilerPassInterface
31 | {
32 | public function process(ContainerBuilder $container): void
33 | {
34 | if ($container->hasParameter('cache.prefix.seed')) {
35 | $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed'));
36 | } else {
37 | $seed = '_'.$container->getParameter('kernel.project_dir');
38 | $seed .= '.'.$container->getParameter('kernel.container_class');
39 | }
40 |
41 | $needsMessageHandler = false;
42 | $allPools = [];
43 | $clearers = [];
44 | $attributes = [
45 | 'provider',
46 | 'name',
47 | 'namespace',
48 | 'default_lifetime',
49 | 'early_expiration_message_bus',
50 | 'reset',
51 | ];
52 | foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) {
53 | $adapter = $pool = $container->getDefinition($id);
54 | if ($pool->isAbstract()) {
55 | continue;
56 | }
57 | $class = $adapter->getClass();
58 | while ($adapter instanceof ChildDefinition) {
59 | $adapter = $container->findDefinition($adapter->getParent());
60 | $class = $class ?: $adapter->getClass();
61 | if ($t = $adapter->getTag('cache.pool')) {
62 | $tags[0] += $t[0];
63 | }
64 | }
65 | $name = $tags[0]['name'] ?? $id;
66 | if (!isset($tags[0]['namespace'])) {
67 | $namespaceSeed = $seed;
68 | if (null !== $class) {
69 | $namespaceSeed .= '.'.$class;
70 | }
71 |
72 | $tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name);
73 | }
74 | if (isset($tags[0]['clearer'])) {
75 | $clearer = $tags[0]['clearer'];
76 | while ($container->hasAlias($clearer)) {
77 | $clearer = (string) $container->getAlias($clearer);
78 | }
79 | } else {
80 | $clearer = null;
81 | }
82 | unset($tags[0]['clearer'], $tags[0]['name']);
83 |
84 | if (isset($tags[0]['provider'])) {
85 | $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider']));
86 | }
87 |
88 | if (ChainAdapter::class === $class) {
89 | $adapters = [];
90 | foreach ($adapter->getArgument(0) as $provider => $adapter) {
91 | if ($adapter instanceof ChildDefinition) {
92 | $chainedPool = $adapter;
93 | } else {
94 | $chainedPool = $adapter = new ChildDefinition($adapter);
95 | }
96 |
97 | $chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]];
98 | $chainedClass = '';
99 |
100 | while ($adapter instanceof ChildDefinition) {
101 | $adapter = $container->findDefinition($adapter->getParent());
102 | $chainedClass = $chainedClass ?: $adapter->getClass();
103 | if ($t = $adapter->getTag('cache.pool')) {
104 | $chainedTags[0] += $t[0];
105 | }
106 | }
107 |
108 | if (ChainAdapter::class === $chainedClass) {
109 | throw new InvalidArgumentException(\sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent()));
110 | }
111 |
112 | $i = 0;
113 |
114 | if (isset($chainedTags[0]['provider'])) {
115 | $chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider'])));
116 | }
117 |
118 | if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) {
119 | $chainedPool->replaceArgument($i++, $tags[0]['namespace']);
120 | }
121 |
122 | if (isset($tags[0]['default_lifetime'])) {
123 | $chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']);
124 | }
125 |
126 | $adapters[] = $chainedPool;
127 | }
128 |
129 | $pool->replaceArgument(0, $adapters);
130 | unset($tags[0]['provider'], $tags[0]['namespace']);
131 | $i = 1;
132 | } else {
133 | $i = 0;
134 | }
135 |
136 | foreach ($attributes as $attr) {
137 | if (!isset($tags[0][$attr])) {
138 | // no-op
139 | } elseif ('reset' === $attr) {
140 | if ($tags[0][$attr]) {
141 | $pool->addTag('kernel.reset', ['method' => $tags[0][$attr]]);
142 | }
143 | } elseif ('early_expiration_message_bus' === $attr) {
144 | $needsMessageHandler = true;
145 | $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class))
146 | ->addArgument(new Reference($tags[0]['early_expiration_message_bus']))
147 | ->addArgument(new Reference('reverse_container'))
148 | ->addArgument((new Definition('callable'))
149 | ->setFactory([new Reference($id), 'setCallbackWrapper'])
150 | ->addArgument(null)
151 | ),
152 | ]);
153 | $pool->addTag('container.reversible');
154 | } elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) {
155 | $argument = $tags[0][$attr];
156 |
157 | if ('default_lifetime' === $attr && !is_numeric($argument)) {
158 | $argument = (new Definition('int', [$argument]))
159 | ->setFactory([ParameterNormalizer::class, 'normalizeDuration']);
160 | }
161 |
162 | $pool->replaceArgument($i++, $argument);
163 | }
164 | unset($tags[0][$attr]);
165 | }
166 | if (!empty($tags[0])) {
167 | throw new InvalidArgumentException(\sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0]))));
168 | }
169 |
170 | if (null !== $clearer) {
171 | $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
172 | }
173 |
174 | $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
175 | }
176 |
177 | if (!$needsMessageHandler) {
178 | $container->removeDefinition('cache.early_expiration_handler');
179 | }
180 |
181 | $notAliasedCacheClearerId = 'cache.global_clearer';
182 | while ($container->hasAlias($notAliasedCacheClearerId)) {
183 | $notAliasedCacheClearerId = (string) $container->getAlias($notAliasedCacheClearerId);
184 | }
185 | if ($container->hasDefinition($notAliasedCacheClearerId)) {
186 | $clearers[$notAliasedCacheClearerId] = $allPools;
187 | }
188 |
189 | foreach ($clearers as $id => $pools) {
190 | $clearer = $container->getDefinition($id);
191 | if ($clearer instanceof ChildDefinition) {
192 | $clearer->replaceArgument(0, $pools);
193 | } else {
194 | $clearer->setArgument(0, $pools);
195 | }
196 | $clearer->addTag('cache.pool.clearer');
197 | }
198 |
199 | $allPoolsKeys = array_keys($allPools);
200 |
201 | if ($container->hasDefinition('console.command.cache_pool_list')) {
202 | $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, $allPoolsKeys);
203 | }
204 |
205 | if ($container->hasDefinition('console.command.cache_pool_clear')) {
206 | $container->getDefinition('console.command.cache_pool_clear')->addArgument($allPoolsKeys);
207 | }
208 |
209 | if ($container->hasDefinition('console.command.cache_pool_delete')) {
210 | $container->getDefinition('console.command.cache_pool_delete')->addArgument($allPoolsKeys);
211 | }
212 | }
213 |
214 | private function getNamespace(string $seed, string $id): string
215 | {
216 | return substr(str_replace('/', '-', base64_encode(hash('xxh128', $id.$seed, true))), 0, 10);
217 | }
218 |
219 | /**
220 | * @internal
221 | */
222 | public static function getServiceProvider(ContainerBuilder $container, string $name): string
223 | {
224 | $container->resolveEnvPlaceholders($name, null, $usedEnvs);
225 |
226 | if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) {
227 | $dsn = $name;
228 |
229 | if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) {
230 | $definition = new Definition(AbstractAdapter::class);
231 | $definition->setFactory([AbstractAdapter::class, 'createConnection']);
232 | $definition->setArguments([$dsn, ['lazy' => true]]);
233 | $container->setDefinition($name, $definition);
234 | }
235 | }
236 |
237 | return $name;
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/DependencyInjection/CachePoolPrunerPass.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\DependencyInjection;
13 |
14 | use Symfony\Component\Cache\PruneableInterface;
15 | use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17 | use Symfony\Component\DependencyInjection\ContainerBuilder;
18 | use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
19 | use Symfony\Component\DependencyInjection\Reference;
20 |
21 | /**
22 | * @author Rob Frawley 2nd
26 | */
27 | final class LockRegistry
28 | {
29 | private static array $openedFiles = [];
30 | private static ?array $lockedFiles = null;
31 | private static \Exception $signalingException;
32 | private static \Closure $signalingCallback;
33 |
34 | /**
35 | * The number of items in this list controls the max number of concurrent processes.
36 | */
37 | private static array $files = [
38 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php',
39 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php',
40 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php',
41 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php',
42 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
43 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
44 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php',
45 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php',
46 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php',
47 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
48 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
49 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php',
50 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php',
51 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php',
52 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php',
53 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php',
54 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php',
55 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php',
56 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php',
57 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php',
58 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php',
59 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php',
60 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php',
61 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php',
62 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php',
63 | ];
64 |
65 | /**
66 | * Defines a set of existing files that will be used as keys to acquire locks.
67 | *
68 | * @return array The previously defined set of files
69 | */
70 | public static function setFiles(array $files): array
71 | {
72 | $previousFiles = self::$files;
73 | self::$files = $files;
74 |
75 | foreach (self::$openedFiles as $file) {
76 | if ($file) {
77 | flock($file, \LOCK_UN);
78 | fclose($file);
79 | }
80 | }
81 | self::$openedFiles = self::$lockedFiles = [];
82 |
83 | return $previousFiles;
84 | }
85 |
86 | public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null): mixed
87 | {
88 | if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) {
89 | // disable locking on Windows by default
90 | self::$files = self::$lockedFiles = [];
91 | }
92 |
93 | $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1;
94 |
95 | if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) {
96 | return $callback($item, $save);
97 | }
98 |
99 | self::$signalingException ??= unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}");
100 | self::$signalingCallback ??= fn () => throw self::$signalingException;
101 |
102 | while (true) {
103 | try {
104 | // race to get the lock in non-blocking mode
105 | $locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock);
106 |
107 | if ($locked || !$wouldBlock) {
108 | $logger?->info(\sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]);
109 | self::$lockedFiles[$key] = true;
110 |
111 | $value = $callback($item, $save);
112 |
113 | if ($save) {
114 | if ($setMetadata) {
115 | $setMetadata($item);
116 | }
117 |
118 | $pool->save($item->set($value));
119 | $save = false;
120 | }
121 |
122 | return $value;
123 | }
124 | // if we failed the race, retry locking in blocking mode to wait for the winner
125 | $logger?->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]);
126 | flock($lock, \LOCK_SH);
127 | } finally {
128 | flock($lock, \LOCK_UN);
129 | unset(self::$lockedFiles[$key]);
130 | }
131 |
132 | try {
133 | $value = $pool->get($item->getKey(), self::$signalingCallback, 0);
134 | $logger?->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]);
135 | $save = false;
136 |
137 | return $value;
138 | } catch (\Exception $e) {
139 | if (self::$signalingException !== $e) {
140 | throw $e;
141 | }
142 | $logger?->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]);
143 | }
144 | }
145 |
146 | return null;
147 | }
148 |
149 | /**
150 | * @return resource|false
151 | */
152 | private static function open(int $key)
153 | {
154 | if (null !== $h = self::$openedFiles[$key] ?? null) {
155 | return $h;
156 | }
157 | set_error_handler(static fn () => null);
158 | try {
159 | $h = fopen(self::$files[$key], 'r+');
160 | } finally {
161 | restore_error_handler();
162 | }
163 |
164 | return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r');
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Marshaller/DefaultMarshaller.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Marshaller;
13 |
14 | use Symfony\Component\Cache\Exception\CacheException;
15 |
16 | /**
17 | * Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise.
18 | *
19 | * @author Nicolas Grekas
20 | */
21 | class DefaultMarshaller implements MarshallerInterface
22 | {
23 | private bool $useIgbinarySerialize = false;
24 | private bool $throwOnSerializationFailure = false;
25 |
26 | public function __construct(?bool $useIgbinarySerialize = null, bool $throwOnSerializationFailure = false)
27 | {
28 | if ($useIgbinarySerialize && (!\extension_loaded('igbinary') || version_compare('3.1.6', phpversion('igbinary'), '>'))) {
29 | throw new CacheException(\extension_loaded('igbinary') ? 'Please upgrade the "igbinary" PHP extension to v3.1.6 or higher.' : 'The "igbinary" PHP extension is not loaded.');
30 | }
31 | $this->useIgbinarySerialize = true === $useIgbinarySerialize;
32 | $this->throwOnSerializationFailure = $throwOnSerializationFailure;
33 | }
34 |
35 | public function marshall(array $values, ?array &$failed): array
36 | {
37 | $serialized = $failed = [];
38 |
39 | foreach ($values as $id => $value) {
40 | try {
41 | if ($this->useIgbinarySerialize) {
42 | $serialized[$id] = igbinary_serialize($value);
43 | } else {
44 | $serialized[$id] = serialize($value);
45 | }
46 | } catch (\Exception $e) {
47 | if ($this->throwOnSerializationFailure) {
48 | throw new \ValueError($e->getMessage(), 0, $e);
49 | }
50 | $failed[] = $id;
51 | }
52 | }
53 |
54 | return $serialized;
55 | }
56 |
57 | public function unmarshall(string $value): mixed
58 | {
59 | if ('b:0;' === $value) {
60 | return false;
61 | }
62 | if ('N;' === $value) {
63 | return null;
64 | }
65 | static $igbinaryNull;
66 | if ($value === $igbinaryNull ??= \extension_loaded('igbinary') ? igbinary_serialize(null) : false) {
67 | return null;
68 | }
69 | $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
70 | try {
71 | if (':' === ($value[1] ?? ':')) {
72 | if (false !== $value = unserialize($value)) {
73 | return $value;
74 | }
75 | } elseif (false === $igbinaryNull) {
76 | throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?');
77 | } elseif (null !== $value = igbinary_unserialize($value)) {
78 | return $value;
79 | }
80 |
81 | throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.');
82 | } catch (\Error $e) {
83 | throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
84 | } finally {
85 | ini_set('unserialize_callback_func', $unserializeCallbackHandler);
86 | }
87 | }
88 |
89 | /**
90 | * @internal
91 | */
92 | public static function handleUnserializeCallback(string $class): never
93 | {
94 | throw new \DomainException('Class not found: '.$class);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Marshaller/DeflateMarshaller.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Marshaller;
13 |
14 | use Symfony\Component\Cache\Exception\CacheException;
15 |
16 | /**
17 | * Compresses values using gzdeflate().
18 | *
19 | * @author Nicolas Grekas
20 | */
21 | class DeflateMarshaller implements MarshallerInterface
22 | {
23 | public function __construct(
24 | private MarshallerInterface $marshaller,
25 | ) {
26 | if (!\function_exists('gzdeflate')) {
27 | throw new CacheException('The "zlib" PHP extension is not loaded.');
28 | }
29 | }
30 |
31 | public function marshall(array $values, ?array &$failed): array
32 | {
33 | return array_map('gzdeflate', $this->marshaller->marshall($values, $failed));
34 | }
35 |
36 | public function unmarshall(string $value): mixed
37 | {
38 | if (false !== $inflatedValue = @gzinflate($value)) {
39 | $value = $inflatedValue;
40 | }
41 |
42 | return $this->marshaller->unmarshall($value);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Marshaller/MarshallerInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Marshaller;
13 |
14 | /**
15 | * Serializes/unserializes PHP values.
16 | *
17 | * Implementations of this interface MUST deal with errors carefully. They MUST
18 | * also deal with forward and backward compatibility at the storage format level.
19 | *
20 | * @author Nicolas Grekas
21 | */
22 | interface MarshallerInterface
23 | {
24 | /**
25 | * Serializes a list of values.
26 | *
27 | * When serialization fails for a specific value, no exception should be
28 | * thrown. Instead, its key should be listed in $failed.
29 | */
30 | public function marshall(array $values, ?array &$failed): array;
31 |
32 | /**
33 | * Unserializes a single value and throws an exception if anything goes wrong.
34 | *
35 | * @throws \Exception Whenever unserialization fails
36 | */
37 | public function unmarshall(string $value): mixed;
38 | }
39 |
--------------------------------------------------------------------------------
/Marshaller/SodiumMarshaller.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Marshaller;
13 |
14 | use Symfony\Component\Cache\Exception\CacheException;
15 | use Symfony\Component\Cache\Exception\InvalidArgumentException;
16 |
17 | /**
18 | * Encrypt/decrypt values using Libsodium.
19 | *
20 | * @author Ahmed TAILOULOUTE
18 | */
19 | class TagAwareMarshaller implements MarshallerInterface
20 | {
21 | private MarshallerInterface $marshaller;
22 |
23 | public function __construct(?MarshallerInterface $marshaller = null)
24 | {
25 | $this->marshaller = $marshaller ?? new DefaultMarshaller();
26 | }
27 |
28 | public function marshall(array $values, ?array &$failed): array
29 | {
30 | $failed = $notSerialized = $serialized = [];
31 |
32 | foreach ($values as $id => $value) {
33 | if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) {
34 | // if the value is an array with keys "tags", "value" and "meta", use a compact serialization format
35 | // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall()
36 |
37 | $v = $this->marshaller->marshall($value, $f);
38 |
39 | if ($f) {
40 | $f = [];
41 | $failed[] = $id;
42 | } else {
43 | if ([] === $value['tags']) {
44 | $v['tags'] = '';
45 | }
46 |
47 | $serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value'];
48 | $serialized[$id][9] = "\x5F";
49 | }
50 | } else {
51 | // other arbitrary values are serialized using the decorated marshaller below
52 | $notSerialized[$id] = $value;
53 | }
54 | }
55 |
56 | if ($notSerialized) {
57 | $serialized += $this->marshaller->marshall($notSerialized, $f);
58 | $failed = array_merge($failed, $f);
59 | }
60 |
61 | return $serialized;
62 | }
63 |
64 | public function unmarshall(string $value): mixed
65 | {
66 | // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
67 | if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) {
68 | return $this->marshaller->unmarshall($value);
69 | }
70 |
71 | // data consists of value, tags and metadata which we need to unpack
72 | $meta = substr($value, 1, 12);
73 | $meta[8] = "\0";
74 | $tagLen = unpack('Nlen', $meta, 8)['len'];
75 | $meta = substr($meta, 0, 8);
76 |
77 | return [
78 | 'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)),
79 | 'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [],
80 | 'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta,
81 | ];
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Messenger/EarlyExpirationDispatcher.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Messenger;
13 |
14 | use Psr\Log\LoggerInterface;
15 | use Symfony\Component\Cache\Adapter\AdapterInterface;
16 | use Symfony\Component\Cache\CacheItem;
17 | use Symfony\Component\DependencyInjection\ReverseContainer;
18 | use Symfony\Component\Messenger\MessageBusInterface;
19 | use Symfony\Component\Messenger\Stamp\HandledStamp;
20 |
21 | /**
22 | * Sends the computation of cached values to a message bus.
23 | */
24 | class EarlyExpirationDispatcher
25 | {
26 | private ?\Closure $callbackWrapper;
27 |
28 | public function __construct(
29 | private MessageBusInterface $bus,
30 | private ReverseContainer $reverseContainer,
31 | ?callable $callbackWrapper = null,
32 | ) {
33 | $this->callbackWrapper = null === $callbackWrapper ? null : $callbackWrapper(...);
34 | }
35 |
36 | public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null): mixed
37 | {
38 | if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) {
39 | // The item is stale or the callback cannot be reversed: we must compute the value now
40 | $logger?->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]);
41 |
42 | return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save);
43 | }
44 |
45 | $envelope = $this->bus->dispatch($message);
46 |
47 | if ($logger) {
48 | if ($envelope->last(HandledStamp::class)) {
49 | $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]);
50 | } else {
51 | $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]);
52 | }
53 | }
54 |
55 | // The item's value is not stale, no need to write it to the backend
56 | $save = false;
57 |
58 | return $message->getItem()->get() ?? $item->get();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Messenger/EarlyExpirationHandler.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Messenger;
13 |
14 | use Symfony\Component\Cache\CacheItem;
15 | use Symfony\Component\DependencyInjection\ReverseContainer;
16 | use Symfony\Component\Messenger\Attribute\AsMessageHandler;
17 |
18 | /**
19 | * Computes cached values sent to a message bus.
20 | */
21 | #[AsMessageHandler]
22 | class EarlyExpirationHandler
23 | {
24 | private array $processedNonces = [];
25 |
26 | public function __construct(
27 | private ReverseContainer $reverseContainer,
28 | ) {
29 | }
30 |
31 | public function __invoke(EarlyExpirationMessage $message): void
32 | {
33 | $item = $message->getItem();
34 | $metadata = $item->getMetadata();
35 | $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0;
36 | $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0;
37 |
38 | if ($expiry && $ctime) {
39 | // skip duplicate or expired messages
40 |
41 | $processingNonce = [$expiry, $ctime];
42 | $pool = $message->getPool();
43 | $key = $item->getKey();
44 |
45 | if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) {
46 | return;
47 | }
48 |
49 | if (microtime(true) >= $expiry) {
50 | return;
51 | }
52 |
53 | $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []);
54 |
55 | if (\count($this->processedNonces[$pool]) > 100) {
56 | array_pop($this->processedNonces[$pool]);
57 | }
58 | }
59 |
60 | static $setMetadata;
61 |
62 | $setMetadata ??= \Closure::bind(
63 | function (CacheItem $item, float $startTime) {
64 | if ($item->expiry > $endTime = microtime(true)) {
65 | $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
66 | $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
67 | }
68 | },
69 | null,
70 | CacheItem::class
71 | );
72 |
73 | $startTime = microtime(true);
74 | $pool = $message->findPool($this->reverseContainer);
75 | $callback = $message->findCallback($this->reverseContainer);
76 | $save = true;
77 | $value = $callback($item, $save);
78 | $setMetadata($item, $startTime);
79 | $pool->save($item->set($value));
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Messenger/EarlyExpirationMessage.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Messenger;
13 |
14 | use Symfony\Component\Cache\Adapter\AdapterInterface;
15 | use Symfony\Component\Cache\CacheItem;
16 | use Symfony\Component\DependencyInjection\ReverseContainer;
17 |
18 | /**
19 | * Conveys a cached value that needs to be computed.
20 | */
21 | final class EarlyExpirationMessage
22 | {
23 | public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self
24 | {
25 | try {
26 | $item = clone $item;
27 | $item->set(null);
28 | } catch (\Exception) {
29 | return null;
30 | }
31 |
32 | $pool = $reverseContainer->getId($pool);
33 |
34 | if (\is_object($callback)) {
35 | if (null === $id = $reverseContainer->getId($callback)) {
36 | return null;
37 | }
38 |
39 | $callback = '@'.$id;
40 | } elseif (!\is_array($callback)) {
41 | $callback = (string) $callback;
42 | } elseif (!\is_object($callback[0])) {
43 | $callback = [(string) $callback[0], (string) $callback[1]];
44 | } else {
45 | if (null === $id = $reverseContainer->getId($callback[0])) {
46 | return null;
47 | }
48 |
49 | $callback = ['@'.$id, (string) $callback[1]];
50 | }
51 |
52 | return new self($item, $pool, $callback);
53 | }
54 |
55 | public function getItem(): CacheItem
56 | {
57 | return $this->item;
58 | }
59 |
60 | public function getPool(): string
61 | {
62 | return $this->pool;
63 | }
64 |
65 | /**
66 | * @return string|string[]
67 | */
68 | public function getCallback(): string|array
69 | {
70 | return $this->callback;
71 | }
72 |
73 | public function findPool(ReverseContainer $reverseContainer): AdapterInterface
74 | {
75 | return $reverseContainer->getService($this->pool);
76 | }
77 |
78 | public function findCallback(ReverseContainer $reverseContainer): callable
79 | {
80 | if (\is_string($callback = $this->callback)) {
81 | return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback;
82 | }
83 | if ('@' === $callback[0][0]) {
84 | $callback[0] = $reverseContainer->getService(substr($callback[0], 1));
85 | }
86 |
87 | return $callback;
88 | }
89 |
90 | private function __construct(
91 | private CacheItem $item,
92 | private string $pool,
93 | private string|array $callback,
94 | ) {
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/PruneableInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache;
13 |
14 | /**
15 | * Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items.
16 | */
17 | interface PruneableInterface
18 | {
19 | public function prune(): bool;
20 | }
21 |
--------------------------------------------------------------------------------
/Psr16Cache.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache;
13 |
14 | use Psr\Cache\CacheException as Psr6CacheException;
15 | use Psr\Cache\CacheItemPoolInterface;
16 | use Psr\SimpleCache\CacheException as SimpleCacheException;
17 | use Psr\SimpleCache\CacheInterface;
18 | use Symfony\Component\Cache\Adapter\AdapterInterface;
19 | use Symfony\Component\Cache\Exception\InvalidArgumentException;
20 | use Symfony\Component\Cache\Traits\ProxyTrait;
21 |
22 | /**
23 | * Turns a PSR-6 cache into a PSR-16 one.
24 | *
25 | * @author Nicolas Grekas
26 | */
27 | class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface
28 | {
29 | use ProxyTrait;
30 |
31 | private ?\Closure $createCacheItem = null;
32 | private ?CacheItem $cacheItemPrototype = null;
33 | private static \Closure $packCacheItem;
34 |
35 | public function __construct(CacheItemPoolInterface $pool)
36 | {
37 | $this->pool = $pool;
38 |
39 | if (!$pool instanceof AdapterInterface) {
40 | return;
41 | }
42 | $cacheItemPrototype = &$this->cacheItemPrototype;
43 | $createCacheItem = \Closure::bind(
44 | static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) {
45 | $item = clone $cacheItemPrototype;
46 | $item->poolHash = $item->innerItem = null;
47 | if ($allowInt && \is_int($key)) {
48 | $item->key = (string) $key;
49 | } else {
50 | \assert('' !== CacheItem::validateKey($key));
51 | $item->key = $key;
52 | }
53 | $item->value = $value;
54 | $item->isHit = false;
55 |
56 | return $item;
57 | },
58 | null,
59 | CacheItem::class
60 | );
61 | $this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) {
62 | if (null === $this->cacheItemPrototype) {
63 | $this->get($allowInt && \is_int($key) ? (string) $key : $key);
64 | }
65 | $this->createCacheItem = $createCacheItem;
66 |
67 | return $createCacheItem($key, null, $allowInt)->set($value);
68 | };
69 | self::$packCacheItem ??= \Closure::bind(
70 | static function (CacheItem $item) {
71 | $item->newMetadata = $item->metadata;
72 |
73 | return $item->pack();
74 | },
75 | null,
76 | CacheItem::class
77 | );
78 | }
79 |
80 | public function get($key, $default = null): mixed
81 | {
82 | try {
83 | $item = $this->pool->getItem($key);
84 | } catch (SimpleCacheException $e) {
85 | throw $e;
86 | } catch (Psr6CacheException $e) {
87 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
88 | }
89 | if (null === $this->cacheItemPrototype) {
90 | $this->cacheItemPrototype = clone $item;
91 | $this->cacheItemPrototype->set(null);
92 | }
93 |
94 | return $item->isHit() ? $item->get() : $default;
95 | }
96 |
97 | public function set($key, $value, $ttl = null): bool
98 | {
99 | try {
100 | if (null !== $f = $this->createCacheItem) {
101 | $item = $f($key, $value);
102 | } else {
103 | $item = $this->pool->getItem($key)->set($value);
104 | }
105 | } catch (SimpleCacheException $e) {
106 | throw $e;
107 | } catch (Psr6CacheException $e) {
108 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
109 | }
110 | if (null !== $ttl) {
111 | $item->expiresAfter($ttl);
112 | }
113 |
114 | return $this->pool->save($item);
115 | }
116 |
117 | public function delete($key): bool
118 | {
119 | try {
120 | return $this->pool->deleteItem($key);
121 | } catch (SimpleCacheException $e) {
122 | throw $e;
123 | } catch (Psr6CacheException $e) {
124 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
125 | }
126 | }
127 |
128 | public function clear(): bool
129 | {
130 | return $this->pool->clear();
131 | }
132 |
133 | public function getMultiple($keys, $default = null): iterable
134 | {
135 | if ($keys instanceof \Traversable) {
136 | $keys = iterator_to_array($keys, false);
137 | } elseif (!\is_array($keys)) {
138 | throw new InvalidArgumentException(\sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys)));
139 | }
140 |
141 | try {
142 | $items = $this->pool->getItems($keys);
143 | } catch (SimpleCacheException $e) {
144 | throw $e;
145 | } catch (Psr6CacheException $e) {
146 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
147 | }
148 | $values = [];
149 |
150 | if (!$this->pool instanceof AdapterInterface) {
151 | foreach ($items as $key => $item) {
152 | $values[$key] = $item->isHit() ? $item->get() : $default;
153 | }
154 |
155 | return $values;
156 | }
157 |
158 | foreach ($items as $key => $item) {
159 | $values[$key] = $item->isHit() ? (self::$packCacheItem)($item) : $default;
160 | }
161 |
162 | return $values;
163 | }
164 |
165 | public function setMultiple($values, $ttl = null): bool
166 | {
167 | $valuesIsArray = \is_array($values);
168 | if (!$valuesIsArray && !$values instanceof \Traversable) {
169 | throw new InvalidArgumentException(\sprintf('Cache values must be array or Traversable, "%s" given.', get_debug_type($values)));
170 | }
171 | $items = [];
172 |
173 | try {
174 | if (null !== $f = $this->createCacheItem) {
175 | $valuesIsArray = false;
176 | foreach ($values as $key => $value) {
177 | $items[$key] = $f($key, $value, true);
178 | }
179 | } elseif ($valuesIsArray) {
180 | $items = [];
181 | foreach ($values as $key => $value) {
182 | $items[] = (string) $key;
183 | }
184 | $items = $this->pool->getItems($items);
185 | } else {
186 | foreach ($values as $key => $value) {
187 | if (\is_int($key)) {
188 | $key = (string) $key;
189 | }
190 | $items[$key] = $this->pool->getItem($key)->set($value);
191 | }
192 | }
193 | } catch (SimpleCacheException $e) {
194 | throw $e;
195 | } catch (Psr6CacheException $e) {
196 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
197 | }
198 | $ok = true;
199 |
200 | foreach ($items as $key => $item) {
201 | if ($valuesIsArray) {
202 | $item->set($values[$key]);
203 | }
204 | if (null !== $ttl) {
205 | $item->expiresAfter($ttl);
206 | }
207 | $ok = $this->pool->saveDeferred($item) && $ok;
208 | }
209 |
210 | return $this->pool->commit() && $ok;
211 | }
212 |
213 | public function deleteMultiple($keys): bool
214 | {
215 | if ($keys instanceof \Traversable) {
216 | $keys = iterator_to_array($keys, false);
217 | } elseif (!\is_array($keys)) {
218 | throw new InvalidArgumentException(\sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys)));
219 | }
220 |
221 | try {
222 | return $this->pool->deleteItems($keys);
223 | } catch (SimpleCacheException $e) {
224 | throw $e;
225 | } catch (Psr6CacheException $e) {
226 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
227 | }
228 | }
229 |
230 | public function has($key): bool
231 | {
232 | try {
233 | return $this->pool->hasItem($key);
234 | } catch (SimpleCacheException $e) {
235 | throw $e;
236 | } catch (Psr6CacheException $e) {
237 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
238 | }
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Symfony PSR-6 implementation for caching
2 | ========================================
3 |
4 | The Cache component provides extended
5 | [PSR-6](https://www.php-fig.org/psr/psr-6/) implementations for adding cache to
6 | your applications. It is designed to have a low overhead so that caching is
7 | fastest. It ships with adapters for the most widespread caching backends.
8 | It also provides a [PSR-16](https://www.php-fig.org/psr/psr-16/) adapter,
9 | and implementations for [symfony/cache-contracts](https://github.com/symfony/cache-contracts)'
10 | `CacheInterface` and `TagAwareCacheInterface`.
11 |
12 | Resources
13 | ---------
14 |
15 | * [Documentation](https://symfony.com/doc/current/components/cache.html)
16 | * [Contributing](https://symfony.com/doc/current/contributing/index.html)
17 | * [Report issues](https://github.com/symfony/symfony/issues) and
18 | [send Pull Requests](https://github.com/symfony/symfony/pulls)
19 | in the [main Symfony repository](https://github.com/symfony/symfony)
20 |
--------------------------------------------------------------------------------
/ResettableInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache;
13 |
14 | use Symfony\Contracts\Service\ResetInterface;
15 |
16 | /**
17 | * Resets a pool's local state.
18 | */
19 | interface ResettableInterface extends ResetInterface
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/Traits/ContractsTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Traits;
13 |
14 | use Psr\Log\LoggerInterface;
15 | use Symfony\Component\Cache\Adapter\AdapterInterface;
16 | use Symfony\Component\Cache\CacheItem;
17 | use Symfony\Component\Cache\Exception\InvalidArgumentException;
18 | use Symfony\Component\Cache\LockRegistry;
19 | use Symfony\Contracts\Cache\CacheInterface;
20 | use Symfony\Contracts\Cache\CacheTrait;
21 | use Symfony\Contracts\Cache\ItemInterface;
22 |
23 | /**
24 | * @author Nicolas Grekas
25 | *
26 | * @internal
27 | */
28 | trait ContractsTrait
29 | {
30 | use CacheTrait {
31 | doGet as private contractsGet;
32 | }
33 |
34 | private \Closure $callbackWrapper;
35 | private array $computing = [];
36 |
37 | /**
38 | * Wraps the callback passed to ->get() in a callable.
39 | *
40 | * @return callable the previous callback wrapper
41 | */
42 | public function setCallbackWrapper(?callable $callbackWrapper): callable
43 | {
44 | if (!isset($this->callbackWrapper)) {
45 | $this->callbackWrapper = LockRegistry::compute(...);
46 |
47 | if (\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {
48 | $this->setCallbackWrapper(null);
49 | }
50 | }
51 |
52 | if (null !== $callbackWrapper && !$callbackWrapper instanceof \Closure) {
53 | $callbackWrapper = $callbackWrapper(...);
54 | }
55 |
56 | $previousWrapper = $this->callbackWrapper;
57 | $this->callbackWrapper = $callbackWrapper ?? static fn (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) => $callback($item, $save);
58 |
59 | return $previousWrapper;
60 | }
61 |
62 | private function doGet(AdapterInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null): mixed
63 | {
64 | if (0 > $beta ??= 1.0) {
65 | throw new InvalidArgumentException(\sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta));
66 | }
67 |
68 | static $setMetadata;
69 |
70 | $setMetadata ??= \Closure::bind(
71 | static function (CacheItem $item, float $startTime, ?array &$metadata) {
72 | if ($item->expiry > $endTime = microtime(true)) {
73 | $item->newMetadata[CacheItem::METADATA_EXPIRY] = $metadata[CacheItem::METADATA_EXPIRY] = $item->expiry;
74 | $item->newMetadata[CacheItem::METADATA_CTIME] = $metadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime));
75 | } else {
76 | unset($metadata[CacheItem::METADATA_EXPIRY], $metadata[CacheItem::METADATA_CTIME], $metadata[CacheItem::METADATA_TAGS]);
77 | }
78 | },
79 | null,
80 | CacheItem::class
81 | );
82 |
83 | $this->callbackWrapper ??= LockRegistry::compute(...);
84 |
85 | return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) {
86 | // don't wrap nor save recursive calls
87 | if (isset($this->computing[$key])) {
88 | $value = $callback($item, $save);
89 | $save = false;
90 |
91 | return $value;
92 | }
93 |
94 | $this->computing[$key] = $key;
95 | $startTime = microtime(true);
96 |
97 | if (!isset($this->callbackWrapper)) {
98 | $this->setCallbackWrapper($this->setCallbackWrapper(null));
99 | }
100 |
101 | try {
102 | $value = ($this->callbackWrapper)($callback, $item, $save, $pool, function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) {
103 | $setMetadata($item, $startTime, $metadata);
104 | }, $this->logger ?? null);
105 | $setMetadata($item, $startTime, $metadata);
106 |
107 | return $value;
108 | } finally {
109 | unset($this->computing[$key]);
110 | }
111 | }, $beta, $metadata, $this->logger ?? null);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Traits/FilesystemCommonTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Traits;
13 |
14 | use Symfony\Component\Cache\Exception\InvalidArgumentException;
15 |
16 | /**
17 | * @author Nicolas Grekas
18 | *
19 | * @internal
20 | */
21 | trait FilesystemCommonTrait
22 | {
23 | private string $directory;
24 | private string $tmpSuffix;
25 |
26 | private function init(string $namespace, ?string $directory): void
27 | {
28 | if (!isset($directory[0])) {
29 | $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache';
30 | } else {
31 | $directory = realpath($directory) ?: $directory;
32 | }
33 | if (isset($namespace[0])) {
34 | if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
35 | throw new InvalidArgumentException(\sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
36 | }
37 | $directory .= \DIRECTORY_SEPARATOR.$namespace;
38 | } else {
39 | $directory .= \DIRECTORY_SEPARATOR.'@';
40 | }
41 | if (!is_dir($directory)) {
42 | @mkdir($directory, 0777, true);
43 | }
44 | $directory .= \DIRECTORY_SEPARATOR;
45 | // On Windows the whole path is limited to 258 chars
46 | if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) {
47 | throw new InvalidArgumentException(\sprintf('Cache directory too long (%s).', $directory));
48 | }
49 |
50 | $this->directory = $directory;
51 | }
52 |
53 | protected function doClear(string $namespace): bool
54 | {
55 | $ok = true;
56 |
57 | foreach ($this->scanHashDir($this->directory) as $file) {
58 | if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) {
59 | continue;
60 | }
61 |
62 | $ok = ($this->doUnlink($file) || !file_exists($file)) && $ok;
63 | }
64 |
65 | return $ok;
66 | }
67 |
68 | protected function doDelete(array $ids): bool
69 | {
70 | $ok = true;
71 |
72 | foreach ($ids as $id) {
73 | $file = $this->getFile($id);
74 | $ok = (!is_file($file) || $this->doUnlink($file) || !file_exists($file)) && $ok;
75 | }
76 |
77 | return $ok;
78 | }
79 |
80 | protected function doUnlink(string $file): bool
81 | {
82 | return @unlink($file);
83 | }
84 |
85 | private function write(string $file, string $data, ?int $expiresAt = null): bool
86 | {
87 | $unlink = false;
88 | set_error_handler(static fn ($type, $message, $file, $line) => throw new \ErrorException($message, 0, $type, $file, $line));
89 | try {
90 | $tmp = $this->directory.$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
91 | try {
92 | $h = fopen($tmp, 'x');
93 | } catch (\ErrorException $e) {
94 | if (!str_contains($e->getMessage(), 'File exists')) {
95 | throw $e;
96 | }
97 |
98 | $tmp = $this->directory.$this->tmpSuffix = str_replace('/', '-', base64_encode(random_bytes(6)));
99 | $h = fopen($tmp, 'x');
100 | }
101 | fwrite($h, $data);
102 | fclose($h);
103 | $unlink = true;
104 |
105 | if (null !== $expiresAt) {
106 | touch($tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds
107 | }
108 |
109 | if ('\\' === \DIRECTORY_SEPARATOR) {
110 | $success = copy($tmp, $file);
111 | } else {
112 | $success = rename($tmp, $file);
113 | $unlink = !$success;
114 | }
115 |
116 | return $success;
117 | } finally {
118 | restore_error_handler();
119 |
120 | if ($unlink) {
121 | @unlink($tmp);
122 | }
123 | }
124 | }
125 |
126 | private function getFile(string $id, bool $mkdir = false, ?string $directory = null): string
127 | {
128 | // Use xxh128 to favor speed over security, which is not an issue here
129 | $hash = str_replace('/', '-', base64_encode(hash('xxh128', static::class.$id, true)));
130 | $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR);
131 |
132 | if ($mkdir && !is_dir($dir)) {
133 | @mkdir($dir, 0777, true);
134 | }
135 |
136 | return $dir.substr($hash, 2, 20);
137 | }
138 |
139 | private function getFileKey(string $file): string
140 | {
141 | return '';
142 | }
143 |
144 | private function scanHashDir(string $directory): \Generator
145 | {
146 | if (!is_dir($directory)) {
147 | return;
148 | }
149 |
150 | $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
151 |
152 | for ($i = 0; $i < 38; ++$i) {
153 | if (!is_dir($directory.$chars[$i])) {
154 | continue;
155 | }
156 |
157 | for ($j = 0; $j < 38; ++$j) {
158 | if (!is_dir($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
159 | continue;
160 | }
161 |
162 | foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) {
163 | if ('.' !== $file && '..' !== $file) {
164 | yield $dir.\DIRECTORY_SEPARATOR.$file;
165 | }
166 | }
167 | }
168 | }
169 | }
170 |
171 | public function __sleep(): array
172 | {
173 | throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
174 | }
175 |
176 | public function __wakeup(): void
177 | {
178 | throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
179 | }
180 |
181 | public function __destruct()
182 | {
183 | if (method_exists(parent::class, '__destruct')) {
184 | parent::__destruct();
185 | }
186 | if (isset($this->tmpSuffix) && is_file($this->directory.$this->tmpSuffix)) {
187 | unlink($this->directory.$this->tmpSuffix);
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/Traits/FilesystemTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Traits;
13 |
14 | use Symfony\Component\Cache\Exception\CacheException;
15 | use Symfony\Component\Cache\Marshaller\MarshallerInterface;
16 |
17 | /**
18 | * @author Nicolas Grekas
19 | * @author Rob Frawley 2nd
19 | *
20 | * @internal
21 | */
22 | trait ProxyTrait
23 | {
24 | private object $pool;
25 |
26 | public function prune(): bool
27 | {
28 | return $this->pool instanceof PruneableInterface && $this->pool->prune();
29 | }
30 |
31 | public function reset(): void
32 | {
33 | if ($this->pool instanceof ResetInterface) {
34 | $this->pool->reset();
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Traits/Redis6ProxyTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Traits;
13 |
14 | if (version_compare(phpversion('redis'), '6.1.0-dev', '>=')) {
15 | /**
16 | * @internal
17 | */
18 | trait Redis6ProxyTrait
19 | {
20 | public function dump($key): \Redis|string|false
21 | {
22 | return $this->initializeLazyObject()->dump(...\func_get_args());
23 | }
24 |
25 | public function hRandField($key, $options = null): \Redis|array|string|false
26 | {
27 | return $this->initializeLazyObject()->hRandField(...\func_get_args());
28 | }
29 |
30 | public function hSet($key, ...$fields_and_vals): \Redis|false|int
31 | {
32 | return $this->initializeLazyObject()->hSet(...\func_get_args());
33 | }
34 |
35 | public function mget($keys): \Redis|array|false
36 | {
37 | return $this->initializeLazyObject()->mget(...\func_get_args());
38 | }
39 |
40 | public function sRandMember($key, $count = 0): mixed
41 | {
42 | return $this->initializeLazyObject()->sRandMember(...\func_get_args());
43 | }
44 |
45 | public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false
46 | {
47 | return $this->initializeLazyObject()->waitaof(...\func_get_args());
48 | }
49 | }
50 | } else {
51 | /**
52 | * @internal
53 | */
54 | trait Redis6ProxyTrait
55 | {
56 | public function dump($key): \Redis|string
57 | {
58 | return $this->initializeLazyObject()->dump(...\func_get_args());
59 | }
60 |
61 | public function hRandField($key, $options = null): \Redis|array|string
62 | {
63 | return $this->initializeLazyObject()->hRandField(...\func_get_args());
64 | }
65 |
66 | public function hSet($key, $member, $value): \Redis|false|int
67 | {
68 | return $this->initializeLazyObject()->hSet(...\func_get_args());
69 | }
70 |
71 | public function mget($keys): \Redis|array
72 | {
73 | return $this->initializeLazyObject()->mget(...\func_get_args());
74 | }
75 |
76 | public function sRandMember($key, $count = 0): \Redis|array|false|string
77 | {
78 | return $this->initializeLazyObject()->sRandMember(...\func_get_args());
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Traits/RedisCluster6ProxyTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Traits;
13 |
14 | if (version_compare(phpversion('redis'), '6.1.0-dev', '>')) {
15 | /**
16 | * @internal
17 | */
18 | trait RedisCluster6ProxyTrait
19 | {
20 | public function getex($key, $options = []): \RedisCluster|string|false
21 | {
22 | return $this->initializeLazyObject()->getex(...\func_get_args());
23 | }
24 |
25 | public function publish($channel, $message): \RedisCluster|bool|int
26 | {
27 | return $this->initializeLazyObject()->publish(...\func_get_args());
28 | }
29 |
30 | public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false
31 | {
32 | return $this->initializeLazyObject()->waitaof(...\func_get_args());
33 | }
34 | }
35 | } else {
36 | /**
37 | * @internal
38 | */
39 | trait RedisCluster6ProxyTrait
40 | {
41 | public function publish($channel, $message): \RedisCluster|bool
42 | {
43 | return $this->initializeLazyObject()->publish(...\func_get_args());
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Traits/RedisClusterNodeProxy.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\Component\Cache\Traits;
13 |
14 | /**
15 | * This file acts as a wrapper to the \RedisCluster implementation so it can accept the same type of calls as
16 | * individual \Redis objects.
17 | *
18 | * Calls are made to individual nodes via: RedisCluster->{method}($host, ...args)'
19 | * according to https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#directed-node-commands
20 | *
21 | * @author Jack Thomas