summaryrefslogtreecommitdiff
path: root/vendor/symfony/cache/Adapter
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/symfony/cache/Adapter')
-rw-r--r--vendor/symfony/cache/Adapter/AbstractAdapter.php191
-rw-r--r--vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php320
-rw-r--r--vendor/symfony/cache/Adapter/AdapterInterface.php35
-rw-r--r--vendor/symfony/cache/Adapter/ApcuAdapter.php116
-rw-r--r--vendor/symfony/cache/Adapter/ArrayAdapter.php359
-rw-r--r--vendor/symfony/cache/Adapter/ChainAdapter.php291
-rw-r--r--vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php237
-rw-r--r--vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php198
-rw-r--r--vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php383
-rw-r--r--vendor/symfony/cache/Adapter/FilesystemAdapter.php29
-rw-r--r--vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php267
-rw-r--r--vendor/symfony/cache/Adapter/MemcachedAdapter.php329
-rw-r--r--vendor/symfony/cache/Adapter/NullAdapter.php105
-rw-r--r--vendor/symfony/cache/Adapter/ParameterNormalizer.php35
-rw-r--r--vendor/symfony/cache/Adapter/PdoAdapter.php398
-rw-r--r--vendor/symfony/cache/Adapter/PhpArrayAdapter.php389
-rw-r--r--vendor/symfony/cache/Adapter/PhpFilesAdapter.php314
-rw-r--r--vendor/symfony/cache/Adapter/ProxyAdapter.php206
-rw-r--r--vendor/symfony/cache/Adapter/Psr16Adapter.php71
-rw-r--r--vendor/symfony/cache/Adapter/RedisAdapter.php25
-rw-r--r--vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php310
-rw-r--r--vendor/symfony/cache/Adapter/TagAwareAdapter.php370
-rw-r--r--vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php31
-rw-r--r--vendor/symfony/cache/Adapter/TraceableAdapter.php250
-rw-r--r--vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php35
25 files changed, 5294 insertions, 0 deletions
diff --git a/vendor/symfony/cache/Adapter/AbstractAdapter.php b/vendor/symfony/cache/Adapter/AbstractAdapter.php
new file mode 100644
index 0000000..5d6336e
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/AbstractAdapter.php
@@ -0,0 +1,191 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Log\LoggerAwareInterface;
15use Psr\Log\LoggerInterface;
16use Symfony\Component\Cache\CacheItem;
17use Symfony\Component\Cache\Exception\InvalidArgumentException;
18use Symfony\Component\Cache\ResettableInterface;
19use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
20use Symfony\Component\Cache\Traits\ContractsTrait;
21use Symfony\Contracts\Cache\CacheInterface;
22
23/**
24 * @author Nicolas Grekas <p@tchwork.com>
25 */
26abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
27{
28 use AbstractAdapterTrait;
29 use ContractsTrait;
30
31 /**
32 * @internal
33 */
34 protected const NS_SEPARATOR = ':';
35
36 private static bool $apcuSupported;
37
38 protected function __construct(string $namespace = '', int $defaultLifetime = 0)
39 {
40 $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
41 $this->defaultLifetime = $defaultLifetime;
42 if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
43 throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
44 }
45 self::$createCacheItem ??= \Closure::bind(
46 static function ($key, $value, $isHit) {
47 $item = new CacheItem();
48 $item->key = $key;
49 $item->value = $value;
50 $item->isHit = $isHit;
51 $item->unpack();
52
53 return $item;
54 },
55 null,
56 CacheItem::class
57 );
58 self::$mergeByLifetime ??= \Closure::bind(
59 static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
60 $byLifetime = [];
61 $now = microtime(true);
62 $expiredIds = [];
63
64 foreach ($deferred as $key => $item) {
65 $key = (string) $key;
66 if (null === $item->expiry) {
67 $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
68 } elseif (!$item->expiry) {
69 $ttl = 0;
70 } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
71 $expiredIds[] = $getId($key);
72 continue;
73 }
74 $byLifetime[$ttl][$getId($key)] = $item->pack();
75 }
76
77 return $byLifetime;
78 },
79 null,
80 CacheItem::class
81 );
82 }
83
84 /**
85 * Returns the best possible adapter that your runtime supports.
86 *
87 * Using ApcuAdapter makes system caches compatible with read-only filesystems.
88 */
89 public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, ?LoggerInterface $logger = null): AdapterInterface
90 {
91 $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
92 if (null !== $logger) {
93 $opcache->setLogger($logger);
94 }
95
96 if (!self::$apcuSupported ??= ApcuAdapter::isSupported()) {
97 return $opcache;
98 }
99
100 if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
101 return $opcache;
102 }
103
104 $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
105 if (null !== $logger) {
106 $apcu->setLogger($logger);
107 }
108
109 return new ChainAdapter([$apcu, $opcache]);
110 }
111
112 public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
113 {
114 if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
115 return RedisAdapter::createConnection($dsn, $options);
116 }
117 if (str_starts_with($dsn, 'memcached:')) {
118 return MemcachedAdapter::createConnection($dsn, $options);
119 }
120 if (str_starts_with($dsn, 'couchbase:')) {
121 if (class_exists('CouchbaseBucket') && CouchbaseBucketAdapter::isSupported()) {
122 return CouchbaseBucketAdapter::createConnection($dsn, $options);
123 }
124
125 return CouchbaseCollectionAdapter::createConnection($dsn, $options);
126 }
127 if (preg_match('/^(mysql|oci|pgsql|sqlsrv|sqlite):/', $dsn)) {
128 return PdoAdapter::createConnection($dsn, $options);
129 }
130
131 throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".');
132 }
133
134 public function commit(): bool
135 {
136 $ok = true;
137 $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, $this->getId(...), $this->defaultLifetime);
138 $retry = $this->deferred = [];
139
140 if ($expiredIds) {
141 try {
142 $this->doDelete($expiredIds);
143 } catch (\Exception $e) {
144 $ok = false;
145 CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
146 }
147 }
148 foreach ($byLifetime as $lifetime => $values) {
149 try {
150 $e = $this->doSave($values, $lifetime);
151 } catch (\Exception $e) {
152 }
153 if (true === $e || [] === $e) {
154 continue;
155 }
156 if (\is_array($e) || 1 === \count($values)) {
157 foreach (\is_array($e) ? $e : array_keys($values) as $id) {
158 $ok = false;
159 $v = $values[$id];
160 $type = get_debug_type($v);
161 $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
162 CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
163 }
164 } else {
165 foreach ($values as $id => $v) {
166 $retry[$lifetime][] = $id;
167 }
168 }
169 }
170
171 // When bulk-save failed, retry each item individually
172 foreach ($retry as $lifetime => $ids) {
173 foreach ($ids as $id) {
174 try {
175 $v = $byLifetime[$lifetime][$id];
176 $e = $this->doSave([$id => $v], $lifetime);
177 } catch (\Exception $e) {
178 }
179 if (true === $e || [] === $e) {
180 continue;
181 }
182 $ok = false;
183 $type = get_debug_type($v);
184 $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
185 CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
186 }
187 }
188
189 return $ok;
190 }
191}
diff --git a/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php b/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php
new file mode 100644
index 0000000..ef62b4f
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php
@@ -0,0 +1,320 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Log\LoggerAwareInterface;
15use Symfony\Component\Cache\CacheItem;
16use Symfony\Component\Cache\Exception\InvalidArgumentException;
17use Symfony\Component\Cache\ResettableInterface;
18use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
19use Symfony\Component\Cache\Traits\ContractsTrait;
20use Symfony\Contracts\Cache\TagAwareCacheInterface;
21
22/**
23 * Abstract for native TagAware adapters.
24 *
25 * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
26 * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
27 *
28 * @author Nicolas Grekas <p@tchwork.com>
29 * @author André Rømcke <andre.romcke+symfony@gmail.com>
30 *
31 * @internal
32 */
33abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
34{
35 use AbstractAdapterTrait;
36 use ContractsTrait;
37
38 private const TAGS_PREFIX = "\1tags\1";
39
40 protected function __construct(string $namespace = '', int $defaultLifetime = 0)
41 {
42 $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
43 $this->defaultLifetime = $defaultLifetime;
44 if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
45 throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
46 }
47 self::$createCacheItem ??= \Closure::bind(
48 static function ($key, $value, $isHit) {
49 $item = new CacheItem();
50 $item->key = $key;
51 $item->isTaggable = true;
52 // If structure does not match what we expect return item as is (no value and not a hit)
53 if (!\is_array($value) || !\array_key_exists('value', $value)) {
54 return $item;
55 }
56 $item->isHit = $isHit;
57 // Extract value, tags and meta data from the cache value
58 $item->value = $value['value'];
59 $item->metadata[CacheItem::METADATA_TAGS] = isset($value['tags']) ? array_combine($value['tags'], $value['tags']) : [];
60 if (isset($value['meta'])) {
61 // For compactness these values are packed, & expiry is offset to reduce size
62 $v = unpack('Ve/Nc', $value['meta']);
63 $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
64 $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
65 }
66
67 return $item;
68 },
69 null,
70 CacheItem::class
71 );
72 self::$mergeByLifetime ??= \Closure::bind(
73 static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
74 $byLifetime = [];
75 $now = microtime(true);
76 $expiredIds = [];
77
78 foreach ($deferred as $key => $item) {
79 $key = (string) $key;
80 if (null === $item->expiry) {
81 $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
82 } elseif (!$item->expiry) {
83 $ttl = 0;
84 } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
85 $expiredIds[] = $getId($key);
86 continue;
87 }
88 // Store Value and Tags on the cache value
89 if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
90 $value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
91 unset($metadata[CacheItem::METADATA_TAGS]);
92 } else {
93 $value = ['value' => $item->value, 'tags' => []];
94 }
95
96 if ($metadata) {
97 // For compactness, expiry and creation duration are packed, using magic numbers as separators
98 $value['meta'] = pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - CacheItem::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME]);
99 }
100
101 // Extract tag changes, these should be removed from values in doSave()
102 $value['tag-operations'] = ['add' => [], 'remove' => []];
103 $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
104 foreach (array_diff_key($value['tags'], $oldTags) as $addedTag) {
105 $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
106 }
107 foreach (array_diff_key($oldTags, $value['tags']) as $removedTag) {
108 $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
109 }
110 $value['tags'] = array_keys($value['tags']);
111
112 $byLifetime[$ttl][$getId($key)] = $value;
113 $item->metadata = $item->newMetadata;
114 }
115
116 return $byLifetime;
117 },
118 null,
119 CacheItem::class
120 );
121 }
122
123 /**
124 * Persists several cache items immediately.
125 *
126 * @param array $values The values to cache, indexed by their cache identifier
127 * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
128 * @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag
129 * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
130 *
131 * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
132 */
133 abstract protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array;
134
135 /**
136 * Removes multiple items from the pool and their corresponding tags.
137 *
138 * @param array $ids An array of identifiers that should be removed from the pool
139 */
140 abstract protected function doDelete(array $ids): bool;
141
142 /**
143 * Removes relations between tags and deleted items.
144 *
145 * @param array $tagData Array of tag => key identifiers that should be removed from the pool
146 */
147 abstract protected function doDeleteTagRelations(array $tagData): bool;
148
149 /**
150 * Invalidates cached items using tags.
151 *
152 * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
153 */
154 abstract protected function doInvalidate(array $tagIds): bool;
155
156 /**
157 * Delete items and yields the tags they were bound to.
158 */
159 protected function doDeleteYieldTags(array $ids): iterable
160 {
161 foreach ($this->doFetch($ids) as $id => $value) {
162 yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
163 }
164
165 $this->doDelete($ids);
166 }
167
168 public function commit(): bool
169 {
170 $ok = true;
171 $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, $this->getId(...), self::TAGS_PREFIX, $this->defaultLifetime);
172 $retry = $this->deferred = [];
173
174 if ($expiredIds) {
175 // Tags are not cleaned up in this case, however that is done on invalidateTags().
176 try {
177 $this->doDelete($expiredIds);
178 } catch (\Exception $e) {
179 $ok = false;
180 CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
181 }
182 }
183 foreach ($byLifetime as $lifetime => $values) {
184 try {
185 $values = $this->extractTagData($values, $addTagData, $removeTagData);
186 $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
187 } catch (\Exception $e) {
188 }
189 if (true === $e || [] === $e) {
190 continue;
191 }
192 if (\is_array($e) || 1 === \count($values)) {
193 foreach (\is_array($e) ? $e : array_keys($values) as $id) {
194 $ok = false;
195 $v = $values[$id];
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->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
199 }
200 } else {
201 foreach ($values as $id => $v) {
202 $retry[$lifetime][] = $id;
203 }
204 }
205 }
206
207 // When bulk-save failed, retry each item individually
208 foreach ($retry as $lifetime => $ids) {
209 foreach ($ids as $id) {
210 try {
211 $v = $byLifetime[$lifetime][$id];
212 $values = $this->extractTagData([$id => $v], $addTagData, $removeTagData);
213 $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
214 } catch (\Exception $e) {
215 }
216 if (true === $e || [] === $e) {
217 continue;
218 }
219 $ok = false;
220 $type = get_debug_type($v);
221 $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
222 CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
223 }
224 }
225
226 return $ok;
227 }
228
229 public function deleteItems(array $keys): bool
230 {
231 if (!$keys) {
232 return true;
233 }
234
235 $ok = true;
236 $ids = [];
237 $tagData = [];
238
239 foreach ($keys as $key) {
240 $ids[$key] = $this->getId($key);
241 unset($this->deferred[$key]);
242 }
243
244 try {
245 foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
246 foreach ($tags as $tag) {
247 $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
248 }
249 }
250 } catch (\Exception) {
251 $ok = false;
252 }
253
254 try {
255 if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
256 return true;
257 }
258 } catch (\Exception) {
259 }
260
261 // When bulk-delete failed, retry each item individually
262 foreach ($ids as $key => $id) {
263 try {
264 $e = null;
265 if ($this->doDelete([$id])) {
266 continue;
267 }
268 } catch (\Exception $e) {
269 }
270 $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
271 CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
272 $ok = false;
273 }
274
275 return $ok;
276 }
277
278 public function invalidateTags(array $tags): bool
279 {
280 if (!$tags) {
281 return false;
282 }
283
284 $tagIds = [];
285 foreach (array_unique($tags) as $tag) {
286 $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
287 }
288
289 try {
290 if ($this->doInvalidate($tagIds)) {
291 return true;
292 }
293 } catch (\Exception $e) {
294 CacheItem::log($this->logger, 'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
295 }
296
297 return false;
298 }
299
300 /**
301 * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
302 */
303 private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
304 {
305 $addTagData = $removeTagData = [];
306 foreach ($values as $id => $value) {
307 foreach ($value['tag-operations']['add'] as $tag => $tagId) {
308 $addTagData[$tagId][] = $id;
309 }
310
311 foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
312 $removeTagData[$tagId][] = $id;
313 }
314
315 unset($values[$id]['tag-operations']);
316 }
317
318 return $values;
319 }
320}
diff --git a/vendor/symfony/cache/Adapter/AdapterInterface.php b/vendor/symfony/cache/Adapter/AdapterInterface.php
new file mode 100644
index 0000000..e556720
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/AdapterInterface.php
@@ -0,0 +1,35 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemPoolInterface;
15use Symfony\Component\Cache\CacheItem;
16
17// Help opcache.preload discover always-needed symbols
18class_exists(CacheItem::class);
19
20/**
21 * Interface for adapters managing instances of Symfony's CacheItem.
22 *
23 * @author Kévin Dunglas <dunglas@gmail.com>
24 */
25interface AdapterInterface extends CacheItemPoolInterface
26{
27 public function getItem(mixed $key): CacheItem;
28
29 /**
30 * @return iterable<string, CacheItem>
31 */
32 public function getItems(array $keys = []): iterable;
33
34 public function clear(string $prefix = ''): bool;
35}
diff --git a/vendor/symfony/cache/Adapter/ApcuAdapter.php b/vendor/symfony/cache/Adapter/ApcuAdapter.php
new file mode 100644
index 0000000..03b512f
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/ApcuAdapter.php
@@ -0,0 +1,116 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\CacheItem;
15use Symfony\Component\Cache\Exception\CacheException;
16use Symfony\Component\Cache\Marshaller\MarshallerInterface;
17
18/**
19 * @author Nicolas Grekas <p@tchwork.com>
20 */
21class 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 try {
102 if (false === $failures = apcu_store($values, null, $lifetime)) {
103 $failures = $values;
104 }
105
106 return array_keys($failures);
107 } catch (\Throwable $e) {
108 if (1 === \count($values)) {
109 // Workaround https://github.com/krakjoe/apcu/issues/170
110 apcu_delete(array_key_first($values));
111 }
112
113 throw $e;
114 }
115 }
116}
diff --git a/vendor/symfony/cache/Adapter/ArrayAdapter.php b/vendor/symfony/cache/Adapter/ArrayAdapter.php
new file mode 100644
index 0000000..0f1c49d
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/ArrayAdapter.php
@@ -0,0 +1,359 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Psr\Log\LoggerAwareInterface;
16use Psr\Log\LoggerAwareTrait;
17use Symfony\Component\Cache\CacheItem;
18use Symfony\Component\Cache\Exception\InvalidArgumentException;
19use Symfony\Component\Cache\ResettableInterface;
20use Symfony\Contracts\Cache\CacheInterface;
21
22/**
23 * An in-memory cache storage.
24 *
25 * Acts as a least-recently-used (LRU) storage when configured with a maximum number of items.
26 *
27 * @author Nicolas Grekas <p@tchwork.com>
28 */
29class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
30{
31 use LoggerAwareTrait;
32
33 private array $values = [];
34 private array $tags = [];
35 private array $expiries = [];
36
37 private static \Closure $createCacheItem;
38
39 /**
40 * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
41 */
42 public function __construct(
43 private int $defaultLifetime = 0,
44 private bool $storeSerialized = true,
45 private float $maxLifetime = 0,
46 private int $maxItems = 0,
47 ) {
48 if (0 > $maxLifetime) {
49 throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime));
50 }
51
52 if (0 > $maxItems) {
53 throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
54 }
55
56 self::$createCacheItem ??= \Closure::bind(
57 static function ($key, $value, $isHit, $tags) {
58 $item = new CacheItem();
59 $item->key = $key;
60 $item->value = $value;
61 $item->isHit = $isHit;
62 if (null !== $tags) {
63 $item->metadata[CacheItem::METADATA_TAGS] = $tags;
64 }
65
66 return $item;
67 },
68 null,
69 CacheItem::class
70 );
71 }
72
73 public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
74 {
75 $item = $this->getItem($key);
76 $metadata = $item->getMetadata();
77
78 // ArrayAdapter works in memory, we don't care about stampede protection
79 if (\INF === $beta || !$item->isHit()) {
80 $save = true;
81 $item->set($callback($item, $save));
82 if ($save) {
83 $this->save($item);
84 }
85 }
86
87 return $item->get();
88 }
89
90 public function delete(string $key): bool
91 {
92 return $this->deleteItem($key);
93 }
94
95 public function hasItem(mixed $key): bool
96 {
97 if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
98 if ($this->maxItems) {
99 // Move the item last in the storage
100 $value = $this->values[$key];
101 unset($this->values[$key]);
102 $this->values[$key] = $value;
103 }
104
105 return true;
106 }
107 \assert('' !== CacheItem::validateKey($key));
108
109 return isset($this->expiries[$key]) && !$this->deleteItem($key);
110 }
111
112 public function getItem(mixed $key): CacheItem
113 {
114 if (!$isHit = $this->hasItem($key)) {
115 $value = null;
116
117 if (!$this->maxItems) {
118 // Track misses in non-LRU mode only
119 $this->values[$key] = null;
120 }
121 } else {
122 $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
123 }
124
125 return (self::$createCacheItem)($key, $value, $isHit, $this->tags[$key] ?? null);
126 }
127
128 public function getItems(array $keys = []): iterable
129 {
130 \assert(self::validateKeys($keys));
131
132 return $this->generateItems($keys, microtime(true), self::$createCacheItem);
133 }
134
135 public function deleteItem(mixed $key): bool
136 {
137 \assert('' !== CacheItem::validateKey($key));
138 unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
139
140 return true;
141 }
142
143 public function deleteItems(array $keys): bool
144 {
145 foreach ($keys as $key) {
146 $this->deleteItem($key);
147 }
148
149 return true;
150 }
151
152 public function save(CacheItemInterface $item): bool
153 {
154 if (!$item instanceof CacheItem) {
155 return false;
156 }
157 $item = (array) $item;
158 $key = $item["\0*\0key"];
159 $value = $item["\0*\0value"];
160 $expiry = $item["\0*\0expiry"];
161
162 $now = microtime(true);
163
164 if (null !== $expiry) {
165 if (!$expiry) {
166 $expiry = \PHP_INT_MAX;
167 } elseif ($expiry <= $now) {
168 $this->deleteItem($key);
169
170 return true;
171 }
172 }
173 if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
174 return false;
175 }
176 if (null === $expiry && 0 < $this->defaultLifetime) {
177 $expiry = $this->defaultLifetime;
178 $expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
179 } elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
180 $expiry = $now + $this->maxLifetime;
181 }
182
183 if ($this->maxItems) {
184 unset($this->values[$key], $this->tags[$key]);
185
186 // Iterate items and vacuum expired ones while we are at it
187 foreach ($this->values as $k => $v) {
188 if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
189 break;
190 }
191
192 unset($this->values[$k], $this->tags[$k], $this->expiries[$k]);
193 }
194 }
195
196 $this->values[$key] = $value;
197 $this->expiries[$key] = $expiry ?? \PHP_INT_MAX;
198
199 if (null === $this->tags[$key] = $item["\0*\0newMetadata"][CacheItem::METADATA_TAGS] ?? null) {
200 unset($this->tags[$key]);
201 }
202
203 return true;
204 }
205
206 public function saveDeferred(CacheItemInterface $item): bool
207 {
208 return $this->save($item);
209 }
210
211 public function commit(): bool
212 {
213 return true;
214 }
215
216 public function clear(string $prefix = ''): bool
217 {
218 if ('' !== $prefix) {
219 $now = microtime(true);
220
221 foreach ($this->values as $key => $value) {
222 if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || str_starts_with($key, $prefix)) {
223 unset($this->values[$key], $this->tags[$key], $this->expiries[$key]);
224 }
225 }
226
227 if ($this->values) {
228 return true;
229 }
230 }
231
232 $this->values = $this->tags = $this->expiries = [];
233
234 return true;
235 }
236
237 /**
238 * Returns all cached values, with cache miss as null.
239 */
240 public function getValues(): array
241 {
242 if (!$this->storeSerialized) {
243 return $this->values;
244 }
245
246 $values = $this->values;
247 foreach ($values as $k => $v) {
248 if (null === $v || 'N;' === $v) {
249 continue;
250 }
251 if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) {
252 $values[$k] = serialize($v);
253 }
254 }
255
256 return $values;
257 }
258
259 public function reset(): void
260 {
261 $this->clear();
262 }
263
264 private function generateItems(array $keys, float $now, \Closure $f): \Generator
265 {
266 foreach ($keys as $i => $key) {
267 if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
268 $value = null;
269
270 if (!$this->maxItems) {
271 // Track misses in non-LRU mode only
272 $this->values[$key] = null;
273 }
274 } else {
275 if ($this->maxItems) {
276 // Move the item last in the storage
277 $value = $this->values[$key];
278 unset($this->values[$key]);
279 $this->values[$key] = $value;
280 }
281
282 $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
283 }
284 unset($keys[$i]);
285
286 yield $key => $f($key, $value, $isHit, $this->tags[$key] ?? null);
287 }
288
289 foreach ($keys as $key) {
290 yield $key => $f($key, null, false);
291 }
292 }
293
294 private function freeze($value, string $key): string|int|float|bool|array|\UnitEnum|null
295 {
296 if (null === $value) {
297 return 'N;';
298 }
299 if (\is_string($value)) {
300 // Serialize strings if they could be confused with serialized objects or arrays
301 if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
302 return serialize($value);
303 }
304 } elseif (!\is_scalar($value)) {
305 try {
306 $serialized = serialize($value);
307 } catch (\Exception $e) {
308 unset($this->values[$key], $this->tags[$key]);
309 $type = get_debug_type($value);
310 $message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
311 CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
312
313 return null;
314 }
315 // Keep value serialized if it contains any objects or any internal references
316 if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) {
317 return $serialized;
318 }
319 }
320
321 return $value;
322 }
323
324 private function unfreeze(string $key, bool &$isHit): mixed
325 {
326 if ('N;' === $value = $this->values[$key]) {
327 return null;
328 }
329 if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
330 try {
331 $value = unserialize($value);
332 } catch (\Exception $e) {
333 CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
334 $value = false;
335 }
336 if (false === $value) {
337 $value = null;
338 $isHit = false;
339
340 if (!$this->maxItems) {
341 $this->values[$key] = null;
342 }
343 }
344 }
345
346 return $value;
347 }
348
349 private function validateKeys(array $keys): bool
350 {
351 foreach ($keys as $key) {
352 if (!\is_string($key) || !isset($this->expiries[$key])) {
353 CacheItem::validateKey($key);
354 }
355 }
356
357 return true;
358 }
359}
diff --git a/vendor/symfony/cache/Adapter/ChainAdapter.php b/vendor/symfony/cache/Adapter/ChainAdapter.php
new file mode 100644
index 0000000..1418cff
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/ChainAdapter.php
@@ -0,0 +1,291 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Psr\Cache\CacheItemPoolInterface;
16use Symfony\Component\Cache\CacheItem;
17use Symfony\Component\Cache\Exception\InvalidArgumentException;
18use Symfony\Component\Cache\PruneableInterface;
19use Symfony\Component\Cache\ResettableInterface;
20use Symfony\Component\Cache\Traits\ContractsTrait;
21use Symfony\Contracts\Cache\CacheInterface;
22use Symfony\Contracts\Service\ResetInterface;
23
24/**
25 * Chains several adapters together.
26 *
27 * Cached items are fetched from the first adapter having them in its data store.
28 * They are saved and deleted in all adapters at once.
29 *
30 * @author Kévin Dunglas <dunglas@gmail.com>
31 */
32class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
33{
34 use ContractsTrait;
35
36 private array $adapters = [];
37 private int $adapterCount;
38
39 private static \Closure $syncItem;
40
41 /**
42 * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items
43 * @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones
44 */
45 public function __construct(
46 array $adapters,
47 private int $defaultLifetime = 0,
48 ) {
49 if (!$adapters) {
50 throw new InvalidArgumentException('At least one adapter must be specified.');
51 }
52
53 foreach ($adapters as $adapter) {
54 if (!$adapter instanceof CacheItemPoolInterface) {
55 throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class));
56 }
57 if ('cli' === \PHP_SAPI && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) {
58 continue; // skip putting APCu in the chain when the backend is disabled
59 }
60
61 if ($adapter instanceof AdapterInterface) {
62 $this->adapters[] = $adapter;
63 } else {
64 $this->adapters[] = new ProxyAdapter($adapter);
65 }
66 }
67 $this->adapterCount = \count($this->adapters);
68
69 self::$syncItem ??= \Closure::bind(
70 static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
71 $sourceItem->isTaggable = false;
72 $sourceMetadata ??= $sourceItem->metadata;
73
74 $item->value = $sourceItem->value;
75 $item->isHit = $sourceItem->isHit;
76 $item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata;
77
78 if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) {
79 $item->expiresAt(\DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
80 } elseif (0 < $defaultLifetime) {
81 $item->expiresAfter($defaultLifetime);
82 }
83
84 return $item;
85 },
86 null,
87 CacheItem::class
88 );
89 }
90
91 public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
92 {
93 $doSave = true;
94 $callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) {
95 $value = $callback($item, $save);
96 $doSave = $save;
97
98 return $value;
99 };
100
101 $wrap = function (?CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$doSave, &$metadata) {
102 static $lastItem;
103 static $i = 0;
104 $adapter = $this->adapters[$i];
105 if (isset($this->adapters[++$i])) {
106 $callback = $wrap;
107 $beta = \INF === $beta ? \INF : 0;
108 }
109 if ($adapter instanceof CacheInterface) {
110 $value = $adapter->get($key, $callback, $beta, $metadata);
111 } else {
112 $value = $this->doGet($adapter, $key, $callback, $beta, $metadata);
113 }
114 if (null !== $item) {
115 (self::$syncItem)($lastItem ??= $item, $item, $this->defaultLifetime, $metadata);
116 }
117 $save = $doSave;
118
119 return $value;
120 };
121
122 return $wrap();
123 }
124
125 public function getItem(mixed $key): CacheItem
126 {
127 $syncItem = self::$syncItem;
128 $misses = [];
129
130 foreach ($this->adapters as $i => $adapter) {
131 $item = $adapter->getItem($key);
132
133 if ($item->isHit()) {
134 while (0 <= --$i) {
135 $this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime));
136 }
137
138 return $item;
139 }
140
141 $misses[$i] = $item;
142 }
143
144 return $item;
145 }
146
147 public function getItems(array $keys = []): iterable
148 {
149 return $this->generateItems($this->adapters[0]->getItems($keys), 0);
150 }
151
152 private function generateItems(iterable $items, int $adapterIndex): \Generator
153 {
154 $missing = [];
155 $misses = [];
156 $nextAdapterIndex = $adapterIndex + 1;
157 $nextAdapter = $this->adapters[$nextAdapterIndex] ?? null;
158
159 foreach ($items as $k => $item) {
160 if (!$nextAdapter || $item->isHit()) {
161 yield $k => $item;
162 } else {
163 $missing[] = $k;
164 $misses[$k] = $item;
165 }
166 }
167
168 if ($missing) {
169 $syncItem = self::$syncItem;
170 $adapter = $this->adapters[$adapterIndex];
171 $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex);
172
173 foreach ($items as $k => $item) {
174 if ($item->isHit()) {
175 $adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime));
176 }
177
178 yield $k => $item;
179 }
180 }
181 }
182
183 public function hasItem(mixed $key): bool
184 {
185 foreach ($this->adapters as $adapter) {
186 if ($adapter->hasItem($key)) {
187 return true;
188 }
189 }
190
191 return false;
192 }
193
194 public function clear(string $prefix = ''): bool
195 {
196 $cleared = true;
197 $i = $this->adapterCount;
198
199 while ($i--) {
200 if ($this->adapters[$i] instanceof AdapterInterface) {
201 $cleared = $this->adapters[$i]->clear($prefix) && $cleared;
202 } else {
203 $cleared = $this->adapters[$i]->clear() && $cleared;
204 }
205 }
206
207 return $cleared;
208 }
209
210 public function deleteItem(mixed $key): bool
211 {
212 $deleted = true;
213 $i = $this->adapterCount;
214
215 while ($i--) {
216 $deleted = $this->adapters[$i]->deleteItem($key) && $deleted;
217 }
218
219 return $deleted;
220 }
221
222 public function deleteItems(array $keys): bool
223 {
224 $deleted = true;
225 $i = $this->adapterCount;
226
227 while ($i--) {
228 $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted;
229 }
230
231 return $deleted;
232 }
233
234 public function save(CacheItemInterface $item): bool
235 {
236 $saved = true;
237 $i = $this->adapterCount;
238
239 while ($i--) {
240 $saved = $this->adapters[$i]->save($item) && $saved;
241 }
242
243 return $saved;
244 }
245
246 public function saveDeferred(CacheItemInterface $item): bool
247 {
248 $saved = true;
249 $i = $this->adapterCount;
250
251 while ($i--) {
252 $saved = $this->adapters[$i]->saveDeferred($item) && $saved;
253 }
254
255 return $saved;
256 }
257
258 public function commit(): bool
259 {
260 $committed = true;
261 $i = $this->adapterCount;
262
263 while ($i--) {
264 $committed = $this->adapters[$i]->commit() && $committed;
265 }
266
267 return $committed;
268 }
269
270 public function prune(): bool
271 {
272 $pruned = true;
273
274 foreach ($this->adapters as $adapter) {
275 if ($adapter instanceof PruneableInterface) {
276 $pruned = $adapter->prune() && $pruned;
277 }
278 }
279
280 return $pruned;
281 }
282
283 public function reset(): void
284 {
285 foreach ($this->adapters as $adapter) {
286 if ($adapter instanceof ResetInterface) {
287 $adapter->reset();
288 }
289 }
290 }
291}
diff --git a/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php b/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php
new file mode 100644
index 0000000..106d7fd
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php
@@ -0,0 +1,237 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Exception\CacheException;
15use Symfony\Component\Cache\Exception\InvalidArgumentException;
16use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
17use Symfony\Component\Cache\Marshaller\MarshallerInterface;
18
19trigger_deprecation('symfony/cache', '7.1', 'The "%s" class is deprecated, use "%s" instead.', CouchbaseBucketAdapter::class, CouchbaseCollectionAdapter::class);
20
21/**
22 * @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
23 *
24 * @deprecated since Symfony 7.1, use {@see CouchbaseCollectionAdapter} instead
25 */
26class CouchbaseBucketAdapter extends AbstractAdapter
27{
28 private const THIRTY_DAYS_IN_SECONDS = 2592000;
29 private const MAX_KEY_LENGTH = 250;
30 private const KEY_NOT_FOUND = 13;
31 private const VALID_DSN_OPTIONS = [
32 'operationTimeout',
33 'configTimeout',
34 'configNodeTimeout',
35 'n1qlTimeout',
36 'httpTimeout',
37 'configDelay',
38 'htconfigIdleTimeout',
39 'durabilityInterval',
40 'durabilityTimeout',
41 ];
42
43 private MarshallerInterface $marshaller;
44
45 public function __construct(
46 private \CouchbaseBucket $bucket,
47 string $namespace = '',
48 int $defaultLifetime = 0,
49 ?MarshallerInterface $marshaller = null,
50 ) {
51 if (!static::isSupported()) {
52 throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
53 }
54
55 $this->maxIdLength = static::MAX_KEY_LENGTH;
56
57 parent::__construct($namespace, $defaultLifetime);
58 $this->enableVersioning();
59 $this->marshaller = $marshaller ?? new DefaultMarshaller();
60 }
61
62 public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \CouchbaseBucket
63 {
64 if (\is_string($servers)) {
65 $servers = [$servers];
66 }
67
68 if (!static::isSupported()) {
69 throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
70 }
71
72 set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
73
74 $dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
75 .'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\?]+))(?:\?(?<options>.*))?$/i';
76
77 $newServers = [];
78 $protocol = 'couchbase';
79 try {
80 $options = self::initOptions($options);
81 $username = $options['username'];
82 $password = $options['password'];
83
84 foreach ($servers as $dsn) {
85 if (!str_starts_with($dsn, 'couchbase:')) {
86 throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
87 }
88
89 preg_match($dsnPattern, $dsn, $matches);
90
91 $username = $matches['username'] ?: $username;
92 $password = $matches['password'] ?: $password;
93 $protocol = $matches['protocol'] ?: $protocol;
94
95 if (isset($matches['options'])) {
96 $optionsInDsn = self::getOptions($matches['options']);
97
98 foreach ($optionsInDsn as $parameter => $value) {
99 $options[$parameter] = $value;
100 }
101 }
102
103 $newServers[] = $matches['host'];
104 }
105
106 $connectionString = $protocol.'://'.implode(',', $newServers);
107
108 $client = new \CouchbaseCluster($connectionString);
109 $client->authenticateAs($username, $password);
110
111 $bucket = $client->openBucket($matches['bucketName']);
112
113 unset($options['username'], $options['password']);
114 foreach ($options as $option => $value) {
115 if ($value) {
116 $bucket->$option = $value;
117 }
118 }
119
120 return $bucket;
121 } finally {
122 restore_error_handler();
123 }
124 }
125
126 public static function isSupported(): bool
127 {
128 return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<');
129 }
130
131 private static function getOptions(string $options): array
132 {
133 $results = [];
134 $optionsInArray = explode('&', $options);
135
136 foreach ($optionsInArray as $option) {
137 [$key, $value] = explode('=', $option);
138
139 if (\in_array($key, static::VALID_DSN_OPTIONS, true)) {
140 $results[$key] = $value;
141 }
142 }
143
144 return $results;
145 }
146
147 private static function initOptions(array $options): array
148 {
149 $options['username'] ??= '';
150 $options['password'] ??= '';
151 $options['operationTimeout'] ??= 0;
152 $options['configTimeout'] ??= 0;
153 $options['configNodeTimeout'] ??= 0;
154 $options['n1qlTimeout'] ??= 0;
155 $options['httpTimeout'] ??= 0;
156 $options['configDelay'] ??= 0;
157 $options['htconfigIdleTimeout'] ??= 0;
158 $options['durabilityInterval'] ??= 0;
159 $options['durabilityTimeout'] ??= 0;
160
161 return $options;
162 }
163
164 protected function doFetch(array $ids): iterable
165 {
166 $resultsCouchbase = $this->bucket->get($ids);
167
168 $results = [];
169 foreach ($resultsCouchbase as $key => $value) {
170 if (null !== $value->error) {
171 continue;
172 }
173 $results[$key] = $this->marshaller->unmarshall($value->value);
174 }
175
176 return $results;
177 }
178
179 protected function doHave(string $id): bool
180 {
181 return false !== $this->bucket->get($id);
182 }
183
184 protected function doClear(string $namespace): bool
185 {
186 if ('' === $namespace) {
187 $this->bucket->manager()->flush();
188
189 return true;
190 }
191
192 return false;
193 }
194
195 protected function doDelete(array $ids): bool
196 {
197 $results = $this->bucket->remove(array_values($ids));
198
199 foreach ($results as $key => $result) {
200 if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) {
201 continue;
202 }
203 unset($results[$key]);
204 }
205
206 return 0 === \count($results);
207 }
208
209 protected function doSave(array $values, int $lifetime): array|bool
210 {
211 if (!$values = $this->marshaller->marshall($values, $failed)) {
212 return $failed;
213 }
214
215 $lifetime = $this->normalizeExpiry($lifetime);
216
217 $ko = [];
218 foreach ($values as $key => $value) {
219 $result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]);
220
221 if (null !== $result->error) {
222 $ko[$key] = $result;
223 }
224 }
225
226 return [] === $ko ? true : $ko;
227 }
228
229 private function normalizeExpiry(int $expiry): int
230 {
231 if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) {
232 $expiry += time();
233 }
234
235 return $expiry;
236 }
237}
diff --git a/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php b/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php
new file mode 100644
index 0000000..9646bc3
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php
@@ -0,0 +1,198 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Couchbase\Bucket;
15use Couchbase\Cluster;
16use Couchbase\ClusterOptions;
17use Couchbase\Collection;
18use Couchbase\DocumentNotFoundException;
19use Couchbase\UpsertOptions;
20use Symfony\Component\Cache\Exception\CacheException;
21use Symfony\Component\Cache\Exception\InvalidArgumentException;
22use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
23use Symfony\Component\Cache\Marshaller\MarshallerInterface;
24
25/**
26 * @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
27 */
28class CouchbaseCollectionAdapter extends AbstractAdapter
29{
30 private const MAX_KEY_LENGTH = 250;
31
32 private MarshallerInterface $marshaller;
33
34 public function __construct(
35 private Collection $connection,
36 string $namespace = '',
37 int $defaultLifetime = 0,
38 ?MarshallerInterface $marshaller = null,
39 ) {
40 if (!static::isSupported()) {
41 throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.');
42 }
43
44 $this->maxIdLength = static::MAX_KEY_LENGTH;
45
46 parent::__construct($namespace, $defaultLifetime);
47 $this->enableVersioning();
48 $this->marshaller = $marshaller ?? new DefaultMarshaller();
49 }
50
51 public static function createConnection(#[\SensitiveParameter] array|string $dsn, array $options = []): Bucket|Collection
52 {
53 if (\is_string($dsn)) {
54 $dsn = [$dsn];
55 }
56
57 if (!static::isSupported()) {
58 throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.');
59 }
60
61 set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
62
63 $pathPattern = '/^(?:\/(?<bucketName>[^\/\?]+))(?:(?:\/(?<scopeName>[^\/]+))(?:\/(?<collectionName>[^\/\?]+)))?(?:\/)?$/';
64 $newServers = [];
65 $protocol = 'couchbase';
66 try {
67 $username = $options['username'] ?? '';
68 $password = $options['password'] ?? '';
69
70 foreach ($dsn as $server) {
71 if (!str_starts_with($server, 'couchbase:')) {
72 throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
73 }
74
75 $params = parse_url($server);
76
77 $username = isset($params['user']) ? rawurldecode($params['user']) : $username;
78 $password = isset($params['pass']) ? rawurldecode($params['pass']) : $password;
79 $protocol = $params['scheme'] ?? $protocol;
80
81 if (isset($params['query'])) {
82 $optionsInDsn = self::getOptions($params['query']);
83
84 foreach ($optionsInDsn as $parameter => $value) {
85 $options[$parameter] = $value;
86 }
87 }
88
89 $newServers[] = $params['host'];
90 }
91
92 $option = isset($params['query']) ? '?'.$params['query'] : '';
93 $connectionString = $protocol.'://'.implode(',', $newServers).$option;
94
95 $clusterOptions = new ClusterOptions();
96 $clusterOptions->credentials($username, $password);
97
98 $client = new Cluster($connectionString, $clusterOptions);
99
100 preg_match($pathPattern, $params['path'] ?? '', $matches);
101 $bucket = $client->bucket($matches['bucketName']);
102 $collection = $bucket->defaultCollection();
103 if (!empty($matches['scopeName'])) {
104 $scope = $bucket->scope($matches['scopeName']);
105 $collection = $scope->collection($matches['collectionName']);
106 }
107
108 return $collection;
109 } finally {
110 restore_error_handler();
111 }
112 }
113
114 public static function isSupported(): bool
115 {
116 return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<');
117 }
118
119 private static function getOptions(string $options): array
120 {
121 $results = [];
122 $optionsInArray = explode('&', $options);
123
124 foreach ($optionsInArray as $option) {
125 [$key, $value] = explode('=', $option);
126
127 $results[$key] = $value;
128 }
129
130 return $results;
131 }
132
133 protected function doFetch(array $ids): array
134 {
135 $results = [];
136 foreach ($ids as $id) {
137 try {
138 $resultCouchbase = $this->connection->get($id);
139 } catch (DocumentNotFoundException) {
140 continue;
141 }
142
143 $content = $resultCouchbase->value ?? $resultCouchbase->content();
144
145 $results[$id] = $this->marshaller->unmarshall($content);
146 }
147
148 return $results;
149 }
150
151 protected function doHave($id): bool
152 {
153 return $this->connection->exists($id)->exists();
154 }
155
156 protected function doClear($namespace): bool
157 {
158 return false;
159 }
160
161 protected function doDelete(array $ids): bool
162 {
163 $idsErrors = [];
164 foreach ($ids as $id) {
165 try {
166 $result = $this->connection->remove($id);
167
168 if (null === $result->mutationToken()) {
169 $idsErrors[] = $id;
170 }
171 } catch (DocumentNotFoundException) {
172 }
173 }
174
175 return 0 === \count($idsErrors);
176 }
177
178 protected function doSave(array $values, $lifetime): array|bool
179 {
180 if (!$values = $this->marshaller->marshall($values, $failed)) {
181 return $failed;
182 }
183
184 $upsertOptions = new UpsertOptions();
185 $upsertOptions->expiry($lifetime);
186
187 $ko = [];
188 foreach ($values as $key => $value) {
189 try {
190 $this->connection->upsert($key, $value, $upsertOptions);
191 } catch (\Exception) {
192 $ko[$key] = '';
193 }
194 }
195
196 return [] === $ko ? true : $ko;
197 }
198}
diff --git a/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php
new file mode 100644
index 0000000..ae2bea7
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php
@@ -0,0 +1,383 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Doctrine\DBAL\ArrayParameterType;
15use Doctrine\DBAL\Configuration;
16use Doctrine\DBAL\Connection;
17use Doctrine\DBAL\DriverManager;
18use Doctrine\DBAL\Exception as DBALException;
19use Doctrine\DBAL\Exception\TableNotFoundException;
20use Doctrine\DBAL\ParameterType;
21use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
22use Doctrine\DBAL\Schema\Schema;
23use Doctrine\DBAL\Tools\DsnParser;
24use Symfony\Component\Cache\Exception\InvalidArgumentException;
25use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
26use Symfony\Component\Cache\Marshaller\MarshallerInterface;
27use Symfony\Component\Cache\PruneableInterface;
28
29class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
30{
31 private const MAX_KEY_LENGTH = 255;
32
33 private MarshallerInterface $marshaller;
34 private Connection $conn;
35 private string $platformName;
36 private string $table = 'cache_items';
37 private string $idCol = 'item_id';
38 private string $dataCol = 'item_data';
39 private string $lifetimeCol = 'item_lifetime';
40 private string $timeCol = 'item_time';
41
42 /**
43 * You can either pass an existing database Doctrine DBAL Connection or
44 * a DSN string that will be used to connect to the database.
45 *
46 * The cache table is created automatically when possible.
47 * Otherwise, use the createTable() method.
48 *
49 * List of available options:
50 * * db_table: The name of the table [default: cache_items]
51 * * db_id_col: The column where to store the cache id [default: item_id]
52 * * db_data_col: The column where to store the cache data [default: item_data]
53 * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
54 * * db_time_col: The column where to store the timestamp [default: item_time]
55 *
56 * @throws InvalidArgumentException When namespace contains invalid characters
57 */
58 public function __construct(
59 Connection|string $connOrDsn,
60 private string $namespace = '',
61 int $defaultLifetime = 0,
62 array $options = [],
63 ?MarshallerInterface $marshaller = null,
64 ) {
65 if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
66 throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
67 }
68
69 if ($connOrDsn instanceof Connection) {
70 $this->conn = $connOrDsn;
71 } else {
72 if (!class_exists(DriverManager::class)) {
73 throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
74 }
75 $params = (new DsnParser([
76 'db2' => 'ibm_db2',
77 'mssql' => 'pdo_sqlsrv',
78 'mysql' => 'pdo_mysql',
79 'mysql2' => 'pdo_mysql',
80 'postgres' => 'pdo_pgsql',
81 'postgresql' => 'pdo_pgsql',
82 'pgsql' => 'pdo_pgsql',
83 'sqlite' => 'pdo_sqlite',
84 'sqlite3' => 'pdo_sqlite',
85 ]))->parse($connOrDsn);
86
87 $config = new Configuration();
88 $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
89
90 $this->conn = DriverManager::getConnection($params, $config);
91 }
92
93 $this->maxIdLength = self::MAX_KEY_LENGTH;
94 $this->table = $options['db_table'] ?? $this->table;
95 $this->idCol = $options['db_id_col'] ?? $this->idCol;
96 $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
97 $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
98 $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
99 $this->marshaller = $marshaller ?? new DefaultMarshaller();
100
101 parent::__construct($namespace, $defaultLifetime);
102 }
103
104 /**
105 * Creates the table to store cache items which can be called once for setup.
106 *
107 * Cache ID are saved in a column of maximum length 255. Cache data is
108 * saved in a BLOB.
109 *
110 * @throws DBALException When the table already exists
111 */
112 public function createTable(): void
113 {
114 $schema = new Schema();
115 $this->addTableToSchema($schema);
116
117 foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
118 $this->conn->executeStatement($sql);
119 }
120 }
121
122 public function configureSchema(Schema $schema, Connection $forConnection, \Closure $isSameDatabase): void
123 {
124 if ($schema->hasTable($this->table)) {
125 return;
126 }
127
128 if ($forConnection !== $this->conn && !$isSameDatabase($this->conn->executeStatement(...))) {
129 return;
130 }
131
132 $this->addTableToSchema($schema);
133 }
134
135 public function prune(): bool
136 {
137 $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
138 $params = [time()];
139 $paramTypes = [ParameterType::INTEGER];
140
141 if ('' !== $this->namespace) {
142 $deleteSql .= " AND $this->idCol LIKE ?";
143 $params[] = sprintf('%s%%', $this->namespace);
144 $paramTypes[] = ParameterType::STRING;
145 }
146
147 try {
148 $this->conn->executeStatement($deleteSql, $params, $paramTypes);
149 } catch (TableNotFoundException) {
150 }
151
152 return true;
153 }
154
155 protected function doFetch(array $ids): iterable
156 {
157 $now = time();
158 $expired = [];
159
160 $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)";
161 $result = $this->conn->executeQuery($sql, [
162 $now,
163 $ids,
164 ], [
165 ParameterType::INTEGER,
166 ArrayParameterType::STRING,
167 ])->iterateNumeric();
168
169 foreach ($result as $row) {
170 if (null === $row[1]) {
171 $expired[] = $row[0];
172 } else {
173 yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
174 }
175 }
176
177 if ($expired) {
178 $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)";
179 $this->conn->executeStatement($sql, [
180 $now,
181 $expired,
182 ], [
183 ParameterType::INTEGER,
184 ArrayParameterType::STRING,
185 ]);
186 }
187 }
188
189 protected function doHave(string $id): bool
190 {
191 $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)";
192 $result = $this->conn->executeQuery($sql, [
193 $id,
194 time(),
195 ], [
196 ParameterType::STRING,
197 ParameterType::INTEGER,
198 ]);
199
200 return (bool) $result->fetchOne();
201 }
202
203 protected function doClear(string $namespace): bool
204 {
205 if ('' === $namespace) {
206 $sql = $this->conn->getDatabasePlatform()->getTruncateTableSQL($this->table);
207 } else {
208 $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
209 }
210
211 try {
212 $this->conn->executeStatement($sql);
213 } catch (TableNotFoundException) {
214 }
215
216 return true;
217 }
218
219 protected function doDelete(array $ids): bool
220 {
221 $sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)";
222 try {
223 $this->conn->executeStatement($sql, [array_values($ids)], [ArrayParameterType::STRING]);
224 } catch (TableNotFoundException) {
225 }
226
227 return true;
228 }
229
230 protected function doSave(array $values, int $lifetime): array|bool
231 {
232 if (!$values = $this->marshaller->marshall($values, $failed)) {
233 return $failed;
234 }
235
236 $platformName = $this->getPlatformName();
237 $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)";
238
239 switch ($platformName) {
240 case 'mysql':
241 $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
242 break;
243 case 'oci':
244 // DUAL is Oracle specific dummy table
245 $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
246 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
247 "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
248 break;
249 case 'sqlsrv':
250 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
251 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
252 $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
253 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
254 "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
255 break;
256 case 'sqlite':
257 $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
258 break;
259 case 'pgsql':
260 $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
261 break;
262 default:
263 $platformName = null;
264 $sql = "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?";
265 break;
266 }
267
268 $now = time();
269 $lifetime = $lifetime ?: null;
270 try {
271 $stmt = $this->conn->prepare($sql);
272 } catch (TableNotFoundException) {
273 if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
274 $this->createTable();
275 }
276 $stmt = $this->conn->prepare($sql);
277 }
278
279 if ('sqlsrv' === $platformName || 'oci' === $platformName) {
280 $bind = static function ($id, $data) use ($stmt) {
281 $stmt->bindValue(1, $id);
282 $stmt->bindValue(2, $id);
283 $stmt->bindValue(3, $data, ParameterType::LARGE_OBJECT);
284 $stmt->bindValue(6, $data, ParameterType::LARGE_OBJECT);
285 };
286 $stmt->bindValue(4, $lifetime, ParameterType::INTEGER);
287 $stmt->bindValue(5, $now, ParameterType::INTEGER);
288 $stmt->bindValue(7, $lifetime, ParameterType::INTEGER);
289 $stmt->bindValue(8, $now, ParameterType::INTEGER);
290 } elseif (null !== $platformName) {
291 $bind = static function ($id, $data) use ($stmt) {
292 $stmt->bindValue(1, $id);
293 $stmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
294 };
295 $stmt->bindValue(3, $lifetime, ParameterType::INTEGER);
296 $stmt->bindValue(4, $now, ParameterType::INTEGER);
297 } else {
298 $stmt->bindValue(2, $lifetime, ParameterType::INTEGER);
299 $stmt->bindValue(3, $now, ParameterType::INTEGER);
300
301 $insertStmt = $this->conn->prepare($insertSql);
302 $insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER);
303 $insertStmt->bindValue(4, $now, ParameterType::INTEGER);
304
305 $bind = static function ($id, $data) use ($stmt, $insertStmt) {
306 $stmt->bindValue(1, $data, ParameterType::LARGE_OBJECT);
307 $stmt->bindValue(4, $id);
308 $insertStmt->bindValue(1, $id);
309 $insertStmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
310 };
311 }
312
313 foreach ($values as $id => $data) {
314 $bind($id, $data);
315 try {
316 $rowCount = $stmt->executeStatement();
317 } catch (TableNotFoundException) {
318 if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
319 $this->createTable();
320 }
321 $rowCount = $stmt->executeStatement();
322 }
323 if (null === $platformName && 0 === $rowCount) {
324 try {
325 $insertStmt->executeStatement();
326 } catch (DBALException) {
327 // A concurrent write won, let it be
328 }
329 }
330 }
331
332 return $failed;
333 }
334
335 /**
336 * @internal
337 */
338 protected function getId(mixed $key): string
339 {
340 if ('pgsql' !== $this->platformName ??= $this->getPlatformName()) {
341 return parent::getId($key);
342 }
343
344 if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
345 $key = rawurlencode($key);
346 }
347
348 return parent::getId($key);
349 }
350
351 private function getPlatformName(): string
352 {
353 if (isset($this->platformName)) {
354 return $this->platformName;
355 }
356
357 $platform = $this->conn->getDatabasePlatform();
358
359 return $this->platformName = match (true) {
360 $platform instanceof \Doctrine\DBAL\Platforms\AbstractMySQLPlatform => 'mysql',
361 $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform => 'sqlite',
362 $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform => 'pgsql',
363 $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform => 'oci',
364 $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform => 'sqlsrv',
365 default => $platform::class,
366 };
367 }
368
369 private function addTableToSchema(Schema $schema): void
370 {
371 $types = [
372 'mysql' => 'binary',
373 'sqlite' => 'text',
374 ];
375
376 $table = $schema->createTable($this->table);
377 $table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]);
378 $table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
379 $table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
380 $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
381 $table->setPrimaryKey([$this->idCol]);
382 }
383}
diff --git a/vendor/symfony/cache/Adapter/FilesystemAdapter.php b/vendor/symfony/cache/Adapter/FilesystemAdapter.php
new file mode 100644
index 0000000..13daa56
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/FilesystemAdapter.php
@@ -0,0 +1,29 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
15use Symfony\Component\Cache\Marshaller\MarshallerInterface;
16use Symfony\Component\Cache\PruneableInterface;
17use Symfony\Component\Cache\Traits\FilesystemTrait;
18
19class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
20{
21 use FilesystemTrait;
22
23 public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null)
24 {
25 $this->marshaller = $marshaller ?? new DefaultMarshaller();
26 parent::__construct('', $defaultLifetime);
27 $this->init($namespace, $directory);
28 }
29}
diff --git a/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php b/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php
new file mode 100644
index 0000000..80edee4
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php
@@ -0,0 +1,267 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Marshaller\MarshallerInterface;
15use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
16use Symfony\Component\Cache\PruneableInterface;
17use Symfony\Component\Cache\Traits\FilesystemTrait;
18
19/**
20 * Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls.
21 *
22 * @author Nicolas Grekas <p@tchwork.com>
23 * @author André Rømcke <andre.romcke+symfony@gmail.com>
24 */
25class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface
26{
27 use FilesystemTrait {
28 prune as private doPrune;
29 doClear as private doClearCache;
30 doSave as private doSaveCache;
31 }
32
33 /**
34 * Folder used for tag symlinks.
35 */
36 private const TAG_FOLDER = 'tags';
37
38 public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null)
39 {
40 $this->marshaller = new TagAwareMarshaller($marshaller);
41 parent::__construct('', $defaultLifetime);
42 $this->init($namespace, $directory);
43 }
44
45 public function prune(): bool
46 {
47 $ok = $this->doPrune();
48
49 set_error_handler(static function () {});
50 $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
51
52 try {
53 foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) {
54 $dir .= \DIRECTORY_SEPARATOR;
55 $keepDir = false;
56 for ($i = 0; $i < 38; ++$i) {
57 if (!is_dir($dir.$chars[$i])) {
58 continue;
59 }
60 for ($j = 0; $j < 38; ++$j) {
61 if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
62 continue;
63 }
64 foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) {
65 if ('.' === $link || '..' === $link) {
66 continue;
67 }
68 if ('_' !== $dir[-2] && realpath($d.\DIRECTORY_SEPARATOR.$link)) {
69 $keepDir = true;
70 } else {
71 unlink($d.\DIRECTORY_SEPARATOR.$link);
72 }
73 }
74 $keepDir ?: rmdir($d);
75 }
76 $keepDir ?: rmdir($dir.$chars[$i]);
77 }
78 $keepDir ?: rmdir($dir);
79 }
80 } finally {
81 restore_error_handler();
82 }
83
84 return $ok;
85 }
86
87 protected function doClear(string $namespace): bool
88 {
89 $ok = $this->doClearCache($namespace);
90
91 if ('' !== $namespace) {
92 return $ok;
93 }
94
95 set_error_handler(static function () {});
96 $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
97
98 $this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
99
100 try {
101 foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) {
102 if (rename($dir, $renamed = substr_replace($dir, $this->tmpSuffix.'_', -9))) {
103 $dir = $renamed.\DIRECTORY_SEPARATOR;
104 } else {
105 $dir .= \DIRECTORY_SEPARATOR;
106 $renamed = null;
107 }
108
109 for ($i = 0; $i < 38; ++$i) {
110 if (!is_dir($dir.$chars[$i])) {
111 continue;
112 }
113 for ($j = 0; $j < 38; ++$j) {
114 if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
115 continue;
116 }
117 foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) {
118 if ('.' !== $link && '..' !== $link && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) {
119 unlink($d.\DIRECTORY_SEPARATOR.$link);
120 }
121 }
122 null === $renamed ?: rmdir($d);
123 }
124 null === $renamed ?: rmdir($dir.$chars[$i]);
125 }
126 null === $renamed ?: rmdir($renamed);
127 }
128 } finally {
129 restore_error_handler();
130 }
131
132 return $ok;
133 }
134
135 protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array
136 {
137 $failed = $this->doSaveCache($values, $lifetime);
138
139 // Add Tags as symlinks
140 foreach ($addTagData as $tagId => $ids) {
141 $tagFolder = $this->getTagFolder($tagId);
142 foreach ($ids as $id) {
143 if ($failed && \in_array($id, $failed, true)) {
144 continue;
145 }
146
147 $file = $this->getFile($id);
148
149 if (!@symlink($file, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) {
150 @unlink($file);
151 $failed[] = $id;
152 }
153 }
154 }
155
156 // Unlink removed Tags
157 foreach ($removeTagData as $tagId => $ids) {
158 $tagFolder = $this->getTagFolder($tagId);
159 foreach ($ids as $id) {
160 if ($failed && \in_array($id, $failed, true)) {
161 continue;
162 }
163
164 @unlink($this->getFile($id, false, $tagFolder));
165 }
166 }
167
168 return $failed;
169 }
170
171 protected function doDeleteYieldTags(array $ids): iterable
172 {
173 foreach ($ids as $id) {
174 $file = $this->getFile($id);
175 if (!is_file($file) || !$h = @fopen($file, 'r')) {
176 continue;
177 }
178
179 if (!@unlink($file)) {
180 fclose($h);
181 continue;
182 }
183
184 $meta = explode("\n", fread($h, 4096), 3)[2] ?? '';
185
186 // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
187 if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) {
188 $meta[9] = "\0";
189 $tagLen = unpack('Nlen', $meta, 9)['len'];
190 $meta = substr($meta, 13, $tagLen);
191
192 if (0 < $tagLen -= \strlen($meta)) {
193 $meta .= fread($h, $tagLen);
194 }
195
196 try {
197 yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta);
198 } catch (\Exception) {
199 yield $id => [];
200 }
201 }
202
203 fclose($h);
204 }
205 }
206
207 protected function doDeleteTagRelations(array $tagData): bool
208 {
209 foreach ($tagData as $tagId => $idList) {
210 $tagFolder = $this->getTagFolder($tagId);
211 foreach ($idList as $id) {
212 @unlink($this->getFile($id, false, $tagFolder));
213 }
214 }
215
216 return true;
217 }
218
219 protected function doInvalidate(array $tagIds): bool
220 {
221 foreach ($tagIds as $tagId) {
222 if (!is_dir($tagFolder = $this->getTagFolder($tagId))) {
223 continue;
224 }
225
226 $this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6)));
227
228 set_error_handler(static function () {});
229
230 try {
231 if (rename($tagFolder, $renamed = substr_replace($tagFolder, $this->tmpSuffix.'_', -10))) {
232 $tagFolder = $renamed.\DIRECTORY_SEPARATOR;
233 } else {
234 $renamed = null;
235 }
236
237 foreach ($this->scanHashDir($tagFolder) as $itemLink) {
238 unlink(realpath($itemLink) ?: $itemLink);
239 unlink($itemLink);
240 }
241
242 if (null === $renamed) {
243 continue;
244 }
245
246 $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
247
248 for ($i = 0; $i < 38; ++$i) {
249 for ($j = 0; $j < 38; ++$j) {
250 rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]);
251 }
252 rmdir($tagFolder.$chars[$i]);
253 }
254 rmdir($renamed);
255 } finally {
256 restore_error_handler();
257 }
258 }
259
260 return true;
261 }
262
263 private function getTagFolder(string $tagId): string
264 {
265 return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
266 }
267}
diff --git a/vendor/symfony/cache/Adapter/MemcachedAdapter.php b/vendor/symfony/cache/Adapter/MemcachedAdapter.php
new file mode 100644
index 0000000..033d987
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/MemcachedAdapter.php
@@ -0,0 +1,329 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Exception\CacheException;
15use Symfony\Component\Cache\Exception\InvalidArgumentException;
16use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
17use Symfony\Component\Cache\Marshaller\MarshallerInterface;
18
19/**
20 * @author Rob Frawley 2nd <rmf@src.run>
21 * @author Nicolas Grekas <p@tchwork.com>
22 */
23class MemcachedAdapter extends AbstractAdapter
24{
25 /**
26 * We are replacing characters that are illegal in Memcached keys with reserved characters from
27 * {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached.
28 * Note: don’t use {@see AbstractAdapter::NS_SEPARATOR}.
29 */
30 private const RESERVED_MEMCACHED = " \n\r\t\v\f\0";
31 private const RESERVED_PSR6 = '@()\{}/';
32 private const MAX_KEY_LENGTH = 250;
33
34 private MarshallerInterface $marshaller;
35 private \Memcached $client;
36 private \Memcached $lazyClient;
37
38 /**
39 * Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged.
40 * Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that:
41 * - the Memcached::OPT_BINARY_PROTOCOL must be enabled
42 * (that's the default when using MemcachedAdapter::createConnection());
43 * - tags eviction by Memcached's LRU algorithm will break by-tags invalidation;
44 * your Memcached memory should be large enough to never trigger LRU.
45 *
46 * Using a MemcachedAdapter as a pure items store is fine.
47 */
48 public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
49 {
50 if (!static::isSupported()) {
51 throw new CacheException('Memcached > 3.1.5 is required.');
52 }
53 $this->maxIdLength = self::MAX_KEY_LENGTH;
54
55 if ('Memcached' === $client::class) {
56 $opt = $client->getOption(\Memcached::OPT_SERIALIZER);
57 if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
58 throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
59 }
60 $this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
61 $this->client = $client;
62 } else {
63 $this->lazyClient = $client;
64 }
65
66 parent::__construct($namespace, $defaultLifetime);
67 $this->enableVersioning();
68 $this->marshaller = $marshaller ?? new DefaultMarshaller();
69 }
70
71 public static function isSupported(): bool
72 {
73 return \extension_loaded('memcached') && version_compare(phpversion('memcached'), '3.1.6', '>=');
74 }
75
76 /**
77 * Creates a Memcached instance.
78 *
79 * By default, the binary protocol, no block, and libketama compatible options are enabled.
80 *
81 * Examples for servers:
82 * - 'memcached://user:pass@localhost?weight=33'
83 * - [['localhost', 11211, 33]]
84 *
85 * @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs
86 *
87 * @throws \ErrorException When invalid options or servers are provided
88 */
89 public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \Memcached
90 {
91 if (\is_string($servers)) {
92 $servers = [$servers];
93 }
94 if (!static::isSupported()) {
95 throw new CacheException('Memcached > 3.1.5 is required.');
96 }
97 set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line));
98 try {
99 $client = new \Memcached($options['persistent_id'] ?? null);
100 $username = $options['username'] ?? null;
101 $password = $options['password'] ?? null;
102
103 // parse any DSN in $servers
104 foreach ($servers as $i => $dsn) {
105 if (\is_array($dsn)) {
106 continue;
107 }
108 if (!str_starts_with($dsn, 'memcached:')) {
109 throw new InvalidArgumentException('Invalid Memcached DSN: it does not start with "memcached:".');
110 }
111 $params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
112 if (!empty($m[2])) {
113 [$username, $password] = explode(':', $m[2], 2) + [1 => null];
114 $username = rawurldecode($username);
115 $password = null !== $password ? rawurldecode($password) : null;
116 }
117
118 return 'file:'.($m[1] ?? '');
119 }, $dsn);
120 if (false === $params = parse_url($params)) {
121 throw new InvalidArgumentException('Invalid Memcached DSN.');
122 }
123 $query = $hosts = [];
124 if (isset($params['query'])) {
125 parse_str($params['query'], $query);
126
127 if (isset($query['host'])) {
128 if (!\is_array($hosts = $query['host'])) {
129 throw new InvalidArgumentException('Invalid Memcached DSN: query parameter "host" must be an array.');
130 }
131 foreach ($hosts as $host => $weight) {
132 if (false === $port = strrpos($host, ':')) {
133 $hosts[$host] = [$host, 11211, (int) $weight];
134 } else {
135 $hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight];
136 }
137 }
138 $hosts = array_values($hosts);
139 unset($query['host']);
140 }
141 if ($hosts && !isset($params['host']) && !isset($params['path'])) {
142 unset($servers[$i]);
143 $servers = array_merge($servers, $hosts);
144 continue;
145 }
146 }
147 if (!isset($params['host']) && !isset($params['path'])) {
148 throw new InvalidArgumentException('Invalid Memcached DSN: missing host or path.');
149 }
150 if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
151 $params['weight'] = $m[1];
152 $params['path'] = substr($params['path'], 0, -\strlen($m[0]));
153 }
154 $params += [
155 'host' => $params['host'] ?? $params['path'],
156 'port' => isset($params['host']) ? 11211 : null,
157 'weight' => 0,
158 ];
159 if ($query) {
160 $params += $query;
161 $options = $query + $options;
162 }
163
164 $servers[$i] = [$params['host'], $params['port'], $params['weight']];
165
166 if ($hosts) {
167 $servers = array_merge($servers, $hosts);
168 }
169 }
170
171 // set client's options
172 unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
173 $options = array_change_key_case($options, \CASE_UPPER);
174 $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
175 $client->setOption(\Memcached::OPT_NO_BLOCK, true);
176 $client->setOption(\Memcached::OPT_TCP_NODELAY, true);
177 if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
178 $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
179 }
180 foreach ($options as $name => $value) {
181 if (\is_int($name)) {
182 continue;
183 }
184 if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
185 $value = \constant('Memcached::'.$name.'_'.strtoupper($value));
186 }
187 unset($options[$name]);
188
189 if (\defined('Memcached::OPT_'.$name)) {
190 $options[\constant('Memcached::OPT_'.$name)] = $value;
191 }
192 }
193 $client->setOptions($options + [\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP]);
194
195 // set client's servers, taking care of persistent connections
196 if (!$client->isPristine()) {
197 $oldServers = [];
198 foreach ($client->getServerList() as $server) {
199 $oldServers[] = [$server['host'], $server['port']];
200 }
201
202 $newServers = [];
203 foreach ($servers as $server) {
204 if (1 < \count($server)) {
205 $server = array_values($server);
206 unset($server[2]);
207 $server[1] = (int) $server[1];
208 }
209 $newServers[] = $server;
210 }
211
212 if ($oldServers !== $newServers) {
213 $client->resetServerList();
214 $client->addServers($servers);
215 }
216 } else {
217 $client->addServers($servers);
218 }
219
220 if (null !== $username || null !== $password) {
221 if (!method_exists($client, 'setSaslAuthData')) {
222 trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
223 }
224 $client->setSaslAuthData($username, $password);
225 }
226
227 return $client;
228 } finally {
229 restore_error_handler();
230 }
231 }
232
233 protected function doSave(array $values, int $lifetime): array|bool
234 {
235 if (!$values = $this->marshaller->marshall($values, $failed)) {
236 return $failed;
237 }
238
239 if ($lifetime && $lifetime > 30 * 86400) {
240 $lifetime += time();
241 }
242
243 $encodedValues = [];
244 foreach ($values as $key => $value) {
245 $encodedValues[self::encodeKey($key)] = $value;
246 }
247
248 return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false;
249 }
250
251 protected function doFetch(array $ids): iterable
252 {
253 try {
254 $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
255
256 $encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds));
257
258 $result = [];
259 foreach ($encodedResult as $key => $value) {
260 $result[self::decodeKey($key)] = $this->marshaller->unmarshall($value);
261 }
262
263 return $result;
264 } catch (\Error $e) {
265 throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
266 }
267 }
268
269 protected function doHave(string $id): bool
270 {
271 return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
272 }
273
274 protected function doDelete(array $ids): bool
275 {
276 $ok = true;
277 $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
278 foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) {
279 if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
280 $ok = false;
281 }
282 }
283
284 return $ok;
285 }
286
287 protected function doClear(string $namespace): bool
288 {
289 return '' === $namespace && $this->getClient()->flush();
290 }
291
292 private function checkResultCode(mixed $result): mixed
293 {
294 $code = $this->client->getResultCode();
295
296 if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
297 return $result;
298 }
299
300 throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage()));
301 }
302
303 private function getClient(): \Memcached
304 {
305 if (isset($this->client)) {
306 return $this->client;
307 }
308
309 $opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER);
310 if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
311 throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
312 }
313 if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) {
314 throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix));
315 }
316
317 return $this->client = $this->lazyClient;
318 }
319
320 private static function encodeKey(string $key): string
321 {
322 return strtr($key, self::RESERVED_MEMCACHED, self::RESERVED_PSR6);
323 }
324
325 private static function decodeKey(string $key): string
326 {
327 return strtr($key, self::RESERVED_PSR6, self::RESERVED_MEMCACHED);
328 }
329}
diff --git a/vendor/symfony/cache/Adapter/NullAdapter.php b/vendor/symfony/cache/Adapter/NullAdapter.php
new file mode 100644
index 0000000..d5d2ef6
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/NullAdapter.php
@@ -0,0 +1,105 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Symfony\Component\Cache\CacheItem;
16use Symfony\Contracts\Cache\CacheInterface;
17
18/**
19 * @author Titouan Galopin <galopintitouan@gmail.com>
20 */
21class NullAdapter implements AdapterInterface, CacheInterface
22{
23 private static \Closure $createCacheItem;
24
25 public function __construct()
26 {
27 self::$createCacheItem ??= \Closure::bind(
28 static function ($key) {
29 $item = new CacheItem();
30 $item->key = $key;
31 $item->isHit = false;
32
33 return $item;
34 },
35 null,
36 CacheItem::class
37 );
38 }
39
40 public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
41 {
42 $save = true;
43
44 return $callback((self::$createCacheItem)($key), $save);
45 }
46
47 public function getItem(mixed $key): CacheItem
48 {
49 return (self::$createCacheItem)($key);
50 }
51
52 public function getItems(array $keys = []): iterable
53 {
54 return $this->generateItems($keys);
55 }
56
57 public function hasItem(mixed $key): bool
58 {
59 return false;
60 }
61
62 public function clear(string $prefix = ''): bool
63 {
64 return true;
65 }
66
67 public function deleteItem(mixed $key): bool
68 {
69 return true;
70 }
71
72 public function deleteItems(array $keys): bool
73 {
74 return true;
75 }
76
77 public function save(CacheItemInterface $item): bool
78 {
79 return true;
80 }
81
82 public function saveDeferred(CacheItemInterface $item): bool
83 {
84 return true;
85 }
86
87 public function commit(): bool
88 {
89 return true;
90 }
91
92 public function delete(string $key): bool
93 {
94 return $this->deleteItem($key);
95 }
96
97 private function generateItems(array $keys): \Generator
98 {
99 $f = self::$createCacheItem;
100
101 foreach ($keys as $key) {
102 yield $key => $f($key);
103 }
104 }
105}
diff --git a/vendor/symfony/cache/Adapter/ParameterNormalizer.php b/vendor/symfony/cache/Adapter/ParameterNormalizer.php
new file mode 100644
index 0000000..a689640
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/ParameterNormalizer.php
@@ -0,0 +1,35 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14/**
15 * @author Lars Strojny <lars@strojny.net>
16 */
17final class ParameterNormalizer
18{
19 public static function normalizeDuration(string $duration): int
20 {
21 if (is_numeric($duration)) {
22 return $duration;
23 }
24
25 if (false !== $time = strtotime($duration, 0)) {
26 return $time;
27 }
28
29 try {
30 return \DateTimeImmutable::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp();
31 } catch (\Exception $e) {
32 throw new \InvalidArgumentException(sprintf('Cannot parse date interval "%s".', $duration), 0, $e);
33 }
34 }
35}
diff --git a/vendor/symfony/cache/Adapter/PdoAdapter.php b/vendor/symfony/cache/Adapter/PdoAdapter.php
new file mode 100644
index 0000000..b18428d
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/PdoAdapter.php
@@ -0,0 +1,398 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Exception\InvalidArgumentException;
15use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
16use Symfony\Component\Cache\Marshaller\MarshallerInterface;
17use Symfony\Component\Cache\PruneableInterface;
18
19class PdoAdapter extends AbstractAdapter implements PruneableInterface
20{
21 private const MAX_KEY_LENGTH = 255;
22
23 private MarshallerInterface $marshaller;
24 private \PDO $conn;
25 private string $dsn;
26 private string $driver;
27 private string $serverVersion;
28 private string $table = 'cache_items';
29 private string $idCol = 'item_id';
30 private string $dataCol = 'item_data';
31 private string $lifetimeCol = 'item_lifetime';
32 private string $timeCol = 'item_time';
33 private ?string $username = null;
34 private ?string $password = null;
35 private array $connectionOptions = [];
36 private string $namespace;
37
38 /**
39 * You can either pass an existing database connection as PDO instance or
40 * a DSN string that will be used to lazy-connect to the database when the
41 * cache is actually used.
42 *
43 * List of available options:
44 * * db_table: The name of the table [default: cache_items]
45 * * db_id_col: The column where to store the cache id [default: item_id]
46 * * db_data_col: The column where to store the cache data [default: item_data]
47 * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
48 * * db_time_col: The column where to store the timestamp [default: item_time]
49 * * db_username: The username when lazy-connect [default: '']
50 * * db_password: The password when lazy-connect [default: '']
51 * * db_connection_options: An array of driver-specific connection options [default: []]
52 *
53 * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
54 * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
55 * @throws InvalidArgumentException When namespace contains invalid characters
56 */
57 public function __construct(#[\SensitiveParameter] \PDO|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
58 {
59 if (\is_string($connOrDsn) && str_contains($connOrDsn, '://')) {
60 throw new InvalidArgumentException(sprintf('Usage of Doctrine DBAL URL with "%s" is not supported. Use a PDO DSN or "%s" instead.', __CLASS__, DoctrineDbalAdapter::class));
61 }
62
63 if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
64 throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
65 }
66
67 if ($connOrDsn instanceof \PDO) {
68 if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
69 throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__));
70 }
71
72 $this->conn = $connOrDsn;
73 } else {
74 $this->dsn = $connOrDsn;
75 }
76
77 $this->maxIdLength = self::MAX_KEY_LENGTH;
78 $this->table = $options['db_table'] ?? $this->table;
79 $this->idCol = $options['db_id_col'] ?? $this->idCol;
80 $this->dataCol = $options['db_data_col'] ?? $this->dataCol;
81 $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
82 $this->timeCol = $options['db_time_col'] ?? $this->timeCol;
83 $this->username = $options['db_username'] ?? $this->username;
84 $this->password = $options['db_password'] ?? $this->password;
85 $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
86 $this->namespace = $namespace;
87 $this->marshaller = $marshaller ?? new DefaultMarshaller();
88
89 parent::__construct($namespace, $defaultLifetime);
90 }
91
92 public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \PDO|string
93 {
94 if ($options['lazy'] ?? true) {
95 return $dsn;
96 }
97
98 $pdo = new \PDO($dsn);
99 $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
100
101 return $pdo;
102 }
103
104 /**
105 * Creates the table to store cache items which can be called once for setup.
106 *
107 * Cache ID are saved in a column of maximum length 255. Cache data is
108 * saved in a BLOB.
109 *
110 * @throws \PDOException When the table already exists
111 * @throws \DomainException When an unsupported PDO driver is used
112 */
113 public function createTable(): void
114 {
115 $sql = match ($driver = $this->getDriver()) {
116 // We use varbinary for the ID column because it prevents unwanted conversions:
117 // - character set conversions between server and client
118 // - trailing space removal
119 // - case-insensitivity
120 // - language processing like é == e
121 'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
122 'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
123 'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
124 'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
125 'sqlsrv' => "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
126 default => throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $driver)),
127 };
128
129 $this->getConnection()->exec($sql);
130 }
131
132 public function prune(): bool
133 {
134 $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";
135
136 if ('' !== $this->namespace) {
137 $deleteSql .= " AND $this->idCol LIKE :namespace";
138 }
139
140 $connection = $this->getConnection();
141
142 try {
143 $delete = $connection->prepare($deleteSql);
144 } catch (\PDOException) {
145 return true;
146 }
147 $delete->bindValue(':time', time(), \PDO::PARAM_INT);
148
149 if ('' !== $this->namespace) {
150 $delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR);
151 }
152 try {
153 return $delete->execute();
154 } catch (\PDOException) {
155 return true;
156 }
157 }
158
159 protected function doFetch(array $ids): iterable
160 {
161 $connection = $this->getConnection();
162
163 $now = time();
164 $expired = [];
165
166 $sql = str_pad('', (\count($ids) << 1) - 1, '?,');
167 $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
168 $stmt = $connection->prepare($sql);
169 $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
170 foreach ($ids as $id) {
171 $stmt->bindValue(++$i, $id);
172 }
173 $result = $stmt->execute();
174
175 if (\is_object($result)) {
176 $result = $result->iterateNumeric();
177 } else {
178 $stmt->setFetchMode(\PDO::FETCH_NUM);
179 $result = $stmt;
180 }
181
182 foreach ($result as $row) {
183 if (null === $row[1]) {
184 $expired[] = $row[0];
185 } else {
186 yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
187 }
188 }
189
190 if ($expired) {
191 $sql = str_pad('', (\count($expired) << 1) - 1, '?,');
192 $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
193 $stmt = $connection->prepare($sql);
194 $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
195 foreach ($expired as $id) {
196 $stmt->bindValue(++$i, $id);
197 }
198 $stmt->execute();
199 }
200 }
201
202 protected function doHave(string $id): bool
203 {
204 $connection = $this->getConnection();
205
206 $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
207 $stmt = $connection->prepare($sql);
208
209 $stmt->bindValue(':id', $id);
210 $stmt->bindValue(':time', time(), \PDO::PARAM_INT);
211 $stmt->execute();
212
213 return (bool) $stmt->fetchColumn();
214 }
215
216 protected function doClear(string $namespace): bool
217 {
218 $conn = $this->getConnection();
219
220 if ('' === $namespace) {
221 if ('sqlite' === $this->getDriver()) {
222 $sql = "DELETE FROM $this->table";
223 } else {
224 $sql = "TRUNCATE TABLE $this->table";
225 }
226 } else {
227 $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
228 }
229
230 try {
231 $conn->exec($sql);
232 } catch (\PDOException) {
233 }
234
235 return true;
236 }
237
238 protected function doDelete(array $ids): bool
239 {
240 $sql = str_pad('', (\count($ids) << 1) - 1, '?,');
241 $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
242 try {
243 $stmt = $this->getConnection()->prepare($sql);
244 $stmt->execute(array_values($ids));
245 } catch (\PDOException) {
246 }
247
248 return true;
249 }
250
251 protected function doSave(array $values, int $lifetime): array|bool
252 {
253 if (!$values = $this->marshaller->marshall($values, $failed)) {
254 return $failed;
255 }
256
257 $conn = $this->getConnection();
258
259 $driver = $this->getDriver();
260 $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
261
262 switch (true) {
263 case 'mysql' === $driver:
264 $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
265 break;
266 case 'oci' === $driver:
267 // DUAL is Oracle specific dummy table
268 $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
269 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
270 "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
271 break;
272 case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
273 // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
274 // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
275 $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
276 "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
277 "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
278 break;
279 case 'sqlite' === $driver:
280 $sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
281 break;
282 case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
283 $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
284 break;
285 default:
286 $driver = null;
287 $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
288 break;
289 }
290
291 $now = time();
292 $lifetime = $lifetime ?: null;
293 try {
294 $stmt = $conn->prepare($sql);
295 } catch (\PDOException $e) {
296 if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
297 $this->createTable();
298 }
299 $stmt = $conn->prepare($sql);
300 }
301
302 // $id and $data are defined later in the loop. Binding is done by reference, values are read on execution.
303 if ('sqlsrv' === $driver || 'oci' === $driver) {
304 $stmt->bindParam(1, $id);
305 $stmt->bindParam(2, $id);
306 $stmt->bindParam(3, $data, \PDO::PARAM_LOB);
307 $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
308 $stmt->bindValue(5, $now, \PDO::PARAM_INT);
309 $stmt->bindParam(6, $data, \PDO::PARAM_LOB);
310 $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
311 $stmt->bindValue(8, $now, \PDO::PARAM_INT);
312 } else {
313 $stmt->bindParam(':id', $id);
314 $stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
315 $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
316 $stmt->bindValue(':time', $now, \PDO::PARAM_INT);
317 }
318 if (null === $driver) {
319 $insertStmt = $conn->prepare($insertSql);
320
321 $insertStmt->bindParam(':id', $id);
322 $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
323 $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
324 $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
325 }
326
327 foreach ($values as $id => $data) {
328 try {
329 $stmt->execute();
330 } catch (\PDOException $e) {
331 if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
332 $this->createTable();
333 }
334 $stmt->execute();
335 }
336 if (null === $driver && !$stmt->rowCount()) {
337 try {
338 $insertStmt->execute();
339 } catch (\PDOException) {
340 // A concurrent write won, let it be
341 }
342 }
343 }
344
345 return $failed;
346 }
347
348 /**
349 * @internal
350 */
351 protected function getId(mixed $key): string
352 {
353 if ('pgsql' !== $this->getDriver()) {
354 return parent::getId($key);
355 }
356
357 if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
358 $key = rawurlencode($key);
359 }
360
361 return parent::getId($key);
362 }
363
364 private function getConnection(): \PDO
365 {
366 if (!isset($this->conn)) {
367 $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
368 $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
369 }
370
371 return $this->conn;
372 }
373
374 private function getDriver(): string
375 {
376 return $this->driver ??= $this->getConnection()->getAttribute(\PDO::ATTR_DRIVER_NAME);
377 }
378
379 private function getServerVersion(): string
380 {
381 return $this->serverVersion ??= $this->getConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION);
382 }
383
384 private function isTableMissing(\PDOException $exception): bool
385 {
386 $driver = $this->getDriver();
387 [$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()];
388
389 return match ($driver) {
390 'pgsql' => '42P01' === $sqlState,
391 'sqlite' => str_contains($exception->getMessage(), 'no such table:'),
392 'oci' => 942 === $code,
393 'sqlsrv' => 208 === $code,
394 'mysql' => 1146 === $code,
395 default => false,
396 };
397 }
398}
diff --git a/vendor/symfony/cache/Adapter/PhpArrayAdapter.php b/vendor/symfony/cache/Adapter/PhpArrayAdapter.php
new file mode 100644
index 0000000..f92a238
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/PhpArrayAdapter.php
@@ -0,0 +1,389 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Psr\Cache\CacheItemPoolInterface;
16use Symfony\Component\Cache\CacheItem;
17use Symfony\Component\Cache\Exception\InvalidArgumentException;
18use Symfony\Component\Cache\PruneableInterface;
19use Symfony\Component\Cache\ResettableInterface;
20use Symfony\Component\Cache\Traits\ContractsTrait;
21use Symfony\Component\Cache\Traits\ProxyTrait;
22use Symfony\Component\VarExporter\VarExporter;
23use Symfony\Contracts\Cache\CacheInterface;
24
25/**
26 * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
27 * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter.
28 *
29 * @author Titouan Galopin <galopintitouan@gmail.com>
30 * @author Nicolas Grekas <p@tchwork.com>
31 */
32class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
33{
34 use ContractsTrait;
35 use ProxyTrait;
36
37 private array $keys;
38 private array $values;
39
40 private static \Closure $createCacheItem;
41 private static array $valuesCache = [];
42
43 /**
44 * @param string $file The PHP file were values are cached
45 * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit
46 */
47 public function __construct(
48 private string $file,
49 AdapterInterface $fallbackPool,
50 ) {
51 $this->pool = $fallbackPool;
52 self::$createCacheItem ??= \Closure::bind(
53 static function ($key, $value, $isHit) {
54 $item = new CacheItem();
55 $item->key = $key;
56 $item->value = $value;
57 $item->isHit = $isHit;
58
59 return $item;
60 },
61 null,
62 CacheItem::class
63 );
64 }
65
66 /**
67 * This adapter takes advantage of how PHP stores arrays in its latest versions.
68 *
69 * @param string $file The PHP file were values are cached
70 * @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
71 */
72 public static function create(string $file, CacheItemPoolInterface $fallbackPool): CacheItemPoolInterface
73 {
74 if (!$fallbackPool instanceof AdapterInterface) {
75 $fallbackPool = new ProxyAdapter($fallbackPool);
76 }
77
78 return new static($file, $fallbackPool);
79 }
80
81 public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
82 {
83 if (!isset($this->values)) {
84 $this->initialize();
85 }
86 if (!isset($this->keys[$key])) {
87 get_from_pool:
88 if ($this->pool instanceof CacheInterface) {
89 return $this->pool->get($key, $callback, $beta, $metadata);
90 }
91
92 return $this->doGet($this->pool, $key, $callback, $beta, $metadata);
93 }
94 $value = $this->values[$this->keys[$key]];
95
96 if ('N;' === $value) {
97 return null;
98 }
99 try {
100 if ($value instanceof \Closure) {
101 return $value();
102 }
103 } catch (\Throwable) {
104 unset($this->keys[$key]);
105 goto get_from_pool;
106 }
107
108 return $value;
109 }
110
111 public function getItem(mixed $key): CacheItem
112 {
113 if (!\is_string($key)) {
114 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
115 }
116 if (!isset($this->values)) {
117 $this->initialize();
118 }
119 if (!isset($this->keys[$key])) {
120 return $this->pool->getItem($key);
121 }
122
123 $value = $this->values[$this->keys[$key]];
124 $isHit = true;
125
126 if ('N;' === $value) {
127 $value = null;
128 } elseif ($value instanceof \Closure) {
129 try {
130 $value = $value();
131 } catch (\Throwable) {
132 $value = null;
133 $isHit = false;
134 }
135 }
136
137 return (self::$createCacheItem)($key, $value, $isHit);
138 }
139
140 public function getItems(array $keys = []): iterable
141 {
142 foreach ($keys as $key) {
143 if (!\is_string($key)) {
144 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
145 }
146 }
147 if (!isset($this->values)) {
148 $this->initialize();
149 }
150
151 return $this->generateItems($keys);
152 }
153
154 public function hasItem(mixed $key): bool
155 {
156 if (!\is_string($key)) {
157 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
158 }
159 if (!isset($this->values)) {
160 $this->initialize();
161 }
162
163 return isset($this->keys[$key]) || $this->pool->hasItem($key);
164 }
165
166 public function deleteItem(mixed $key): bool
167 {
168 if (!\is_string($key)) {
169 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
170 }
171 if (!isset($this->values)) {
172 $this->initialize();
173 }
174
175 return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
176 }
177
178 public function deleteItems(array $keys): bool
179 {
180 $deleted = true;
181 $fallbackKeys = [];
182
183 foreach ($keys as $key) {
184 if (!\is_string($key)) {
185 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
186 }
187
188 if (isset($this->keys[$key])) {
189 $deleted = false;
190 } else {
191 $fallbackKeys[] = $key;
192 }
193 }
194 if (!isset($this->values)) {
195 $this->initialize();
196 }
197
198 if ($fallbackKeys) {
199 $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted;
200 }
201
202 return $deleted;
203 }
204
205 public function save(CacheItemInterface $item): bool
206 {
207 if (!isset($this->values)) {
208 $this->initialize();
209 }
210
211 return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
212 }
213
214 public function saveDeferred(CacheItemInterface $item): bool
215 {
216 if (!isset($this->values)) {
217 $this->initialize();
218 }
219
220 return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
221 }
222
223 public function commit(): bool
224 {
225 return $this->pool->commit();
226 }
227
228 public function clear(string $prefix = ''): bool
229 {
230 $this->keys = $this->values = [];
231
232 $cleared = @unlink($this->file) || !file_exists($this->file);
233 unset(self::$valuesCache[$this->file]);
234
235 if ($this->pool instanceof AdapterInterface) {
236 return $this->pool->clear($prefix) && $cleared;
237 }
238
239 return $this->pool->clear() && $cleared;
240 }
241
242 /**
243 * Store an array of cached values.
244 *
245 * @param array $values The cached values
246 *
247 * @return string[] A list of classes to preload on PHP 7.4+
248 */
249 public function warmUp(array $values): array
250 {
251 if (file_exists($this->file)) {
252 if (!is_file($this->file)) {
253 throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file));
254 }
255
256 if (!is_writable($this->file)) {
257 throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file));
258 }
259 } else {
260 $directory = \dirname($this->file);
261
262 if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
263 throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory));
264 }
265
266 if (!is_writable($directory)) {
267 throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory));
268 }
269 }
270
271 $preload = [];
272 $dumpedValues = '';
273 $dumpedMap = [];
274 $dump = <<<'EOF'
275<?php
276
277// This file has been auto-generated by the Symfony Cache Component.
278
279return [[
280
281
282EOF;
283
284 foreach ($values as $key => $value) {
285 CacheItem::validateKey(\is_int($key) ? (string) $key : $key);
286 $isStaticValue = true;
287
288 if (null === $value) {
289 $value = "'N;'";
290 } elseif (\is_object($value) || \is_array($value)) {
291 try {
292 $value = VarExporter::export($value, $isStaticValue, $preload);
293 } catch (\Exception $e) {
294 throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
295 }
296 } elseif (\is_string($value)) {
297 // Wrap "N;" in a closure to not confuse it with an encoded `null`
298 if ('N;' === $value) {
299 $isStaticValue = false;
300 }
301 $value = var_export($value, true);
302 } elseif (!\is_scalar($value)) {
303 throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
304 } else {
305 $value = var_export($value, true);
306 }
307
308 if (!$isStaticValue) {
309 $value = str_replace("\n", "\n ", $value);
310 $value = "static function () {\n return {$value};\n}";
311 }
312 $hash = hash('xxh128', $value);
313
314 if (null === $id = $dumpedMap[$hash] ?? null) {
315 $id = $dumpedMap[$hash] = \count($dumpedMap);
316 $dumpedValues .= "{$id} => {$value},\n";
317 }
318
319 $dump .= var_export($key, true)." => {$id},\n";
320 }
321
322 $dump .= "\n], [\n\n{$dumpedValues}\n]];\n";
323
324 $tmpFile = uniqid($this->file, true);
325
326 file_put_contents($tmpFile, $dump);
327 @chmod($tmpFile, 0666 & ~umask());
328 unset($serialized, $value, $dump);
329
330 @rename($tmpFile, $this->file);
331 unset(self::$valuesCache[$this->file]);
332
333 $this->initialize();
334
335 return $preload;
336 }
337
338 /**
339 * Load the cache file.
340 */
341 private function initialize(): void
342 {
343 if (isset(self::$valuesCache[$this->file])) {
344 $values = self::$valuesCache[$this->file];
345 } elseif (!is_file($this->file)) {
346 $this->keys = $this->values = [];
347
348 return;
349 } else {
350 $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
351 }
352
353 if (2 !== \count($values) || !isset($values[0], $values[1])) {
354 $this->keys = $this->values = [];
355 } else {
356 [$this->keys, $this->values] = $values;
357 }
358 }
359
360 private function generateItems(array $keys): \Generator
361 {
362 $f = self::$createCacheItem;
363 $fallbackKeys = [];
364
365 foreach ($keys as $key) {
366 if (isset($this->keys[$key])) {
367 $value = $this->values[$this->keys[$key]];
368
369 if ('N;' === $value) {
370 yield $key => $f($key, null, true);
371 } elseif ($value instanceof \Closure) {
372 try {
373 yield $key => $f($key, $value(), true);
374 } catch (\Throwable) {
375 yield $key => $f($key, null, false);
376 }
377 } else {
378 yield $key => $f($key, $value, true);
379 }
380 } else {
381 $fallbackKeys[] = $key;
382 }
383 }
384
385 if ($fallbackKeys) {
386 yield from $this->pool->getItems($fallbackKeys);
387 }
388 }
389}
diff --git a/vendor/symfony/cache/Adapter/PhpFilesAdapter.php b/vendor/symfony/cache/Adapter/PhpFilesAdapter.php
new file mode 100644
index 0000000..917ff16
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/PhpFilesAdapter.php
@@ -0,0 +1,314 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Exception\CacheException;
15use Symfony\Component\Cache\Exception\InvalidArgumentException;
16use Symfony\Component\Cache\PruneableInterface;
17use Symfony\Component\Cache\Traits\FilesystemCommonTrait;
18use Symfony\Component\VarExporter\VarExporter;
19
20/**
21 * @author Piotr Stankowski <git@trakos.pl>
22 * @author Nicolas Grekas <p@tchwork.com>
23 * @author Rob Frawley 2nd <rmf@src.run>
24 */
25class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
26{
27 use FilesystemCommonTrait {
28 doClear as private doCommonClear;
29 doDelete as private doCommonDelete;
30 }
31
32 private \Closure $includeHandler;
33 private array $values = [];
34 private array $files = [];
35
36 private static int $startTime;
37 private static array $valuesCache = [];
38
39 /**
40 * @param bool $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
41 * Doing so is encouraged because it fits perfectly OPcache's memory model.
42 *
43 * @throws CacheException if OPcache is not enabled
44 */
45 public function __construct(
46 string $namespace = '',
47 int $defaultLifetime = 0,
48 ?string $directory = null,
49 private bool $appendOnly = false,
50 ) {
51 self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
52 parent::__construct('', $defaultLifetime);
53 $this->init($namespace, $directory);
54 $this->includeHandler = static function ($type, $msg, $file, $line) {
55 throw new \ErrorException($msg, 0, $type, $file, $line);
56 };
57 }
58
59 public static function isSupported(): bool
60 {
61 self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time();
62
63 return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL));
64 }
65
66 public function prune(): bool
67 {
68 $time = time();
69 $pruned = true;
70 $getExpiry = true;
71
72 set_error_handler($this->includeHandler);
73 try {
74 foreach ($this->scanHashDir($this->directory) as $file) {
75 try {
76 if (\is_array($expiresAt = include $file)) {
77 $expiresAt = $expiresAt[0];
78 }
79 } catch (\ErrorException $e) {
80 $expiresAt = $time;
81 }
82
83 if ($time >= $expiresAt) {
84 $pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned;
85 }
86 }
87 } finally {
88 restore_error_handler();
89 }
90
91 return $pruned;
92 }
93
94 protected function doFetch(array $ids): iterable
95 {
96 if ($this->appendOnly) {
97 $now = 0;
98 $missingIds = [];
99 } else {
100 $now = time();
101 $missingIds = $ids;
102 $ids = [];
103 }
104 $values = [];
105
106 begin:
107 $getExpiry = false;
108
109 foreach ($ids as $id) {
110 if (null === $value = $this->values[$id] ?? null) {
111 $missingIds[] = $id;
112 } elseif ('N;' === $value) {
113 $values[$id] = null;
114 } elseif (!\is_object($value)) {
115 $values[$id] = $value;
116 } elseif (!$value instanceof LazyValue) {
117 $values[$id] = $value();
118 } elseif (false === $values[$id] = include $value->file) {
119 unset($values[$id], $this->values[$id]);
120 $missingIds[] = $id;
121 }
122 if (!$this->appendOnly) {
123 unset($this->values[$id]);
124 }
125 }
126
127 if (!$missingIds) {
128 return $values;
129 }
130
131 set_error_handler($this->includeHandler);
132 try {
133 $getExpiry = true;
134
135 foreach ($missingIds as $k => $id) {
136 try {
137 $file = $this->files[$id] ??= $this->getFile($id);
138
139 if (isset(self::$valuesCache[$file])) {
140 [$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
141 } elseif (\is_array($expiresAt = include $file)) {
142 if ($this->appendOnly) {
143 self::$valuesCache[$file] = $expiresAt;
144 }
145
146 [$expiresAt, $this->values[$id]] = $expiresAt;
147 } elseif ($now < $expiresAt) {
148 $this->values[$id] = new LazyValue($file);
149 }
150
151 if ($now >= $expiresAt) {
152 unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
153 }
154 } catch (\ErrorException $e) {
155 unset($missingIds[$k]);
156 }
157 }
158 } finally {
159 restore_error_handler();
160 }
161
162 $ids = $missingIds;
163 $missingIds = [];
164 goto begin;
165 }
166
167 protected function doHave(string $id): bool
168 {
169 if ($this->appendOnly && isset($this->values[$id])) {
170 return true;
171 }
172
173 set_error_handler($this->includeHandler);
174 try {
175 $file = $this->files[$id] ??= $this->getFile($id);
176 $getExpiry = true;
177
178 if (isset(self::$valuesCache[$file])) {
179 [$expiresAt, $value] = self::$valuesCache[$file];
180 } elseif (\is_array($expiresAt = include $file)) {
181 if ($this->appendOnly) {
182 self::$valuesCache[$file] = $expiresAt;
183 }
184
185 [$expiresAt, $value] = $expiresAt;
186 } elseif ($this->appendOnly) {
187 $value = new LazyValue($file);
188 }
189 } catch (\ErrorException) {
190 return false;
191 } finally {
192 restore_error_handler();
193 }
194 if ($this->appendOnly) {
195 $now = 0;
196 $this->values[$id] = $value;
197 } else {
198 $now = time();
199 }
200
201 return $now < $expiresAt;
202 }
203
204 protected function doSave(array $values, int $lifetime): array|bool
205 {
206 $ok = true;
207 $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
208 $allowCompile = self::isSupported();
209
210 foreach ($values as $key => $value) {
211 unset($this->values[$key]);
212 $isStaticValue = true;
213 if (null === $value) {
214 $value = "'N;'";
215 } elseif (\is_object($value) || \is_array($value)) {
216 try {
217 $value = VarExporter::export($value, $isStaticValue);
218 } catch (\Exception $e) {
219 throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
220 }
221 } elseif (\is_string($value)) {
222 // Wrap "N;" in a closure to not confuse it with an encoded `null`
223 if ('N;' === $value) {
224 $isStaticValue = false;
225 }
226 $value = var_export($value, true);
227 } elseif (!\is_scalar($value)) {
228 throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
229 } else {
230 $value = var_export($value, true);
231 }
232
233 $encodedKey = rawurlencode($key);
234
235 if ($isStaticValue) {
236 $value = "return [{$expiry}, {$value}];";
237 } elseif ($this->appendOnly) {
238 $value = "return [{$expiry}, static fn () => {$value}];";
239 } else {
240 // We cannot use a closure here because of https://bugs.php.net/76982
241 $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
242 $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
243 }
244
245 $file = $this->files[$key] = $this->getFile($key, true);
246 // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past
247 $ok = $this->write($file, "<?php //{$encodedKey}\n\n{$value}\n", self::$startTime - 10) && $ok;
248
249 if ($allowCompile) {
250 @opcache_invalidate($file, true);
251 @opcache_compile_file($file);
252 }
253 unset(self::$valuesCache[$file]);
254 }
255
256 if (!$ok && !is_writable($this->directory)) {
257 throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
258 }
259
260 return $ok;
261 }
262
263 protected function doClear(string $namespace): bool
264 {
265 $this->values = [];
266
267 return $this->doCommonClear($namespace);
268 }
269
270 protected function doDelete(array $ids): bool
271 {
272 foreach ($ids as $id) {
273 unset($this->values[$id]);
274 }
275
276 return $this->doCommonDelete($ids);
277 }
278
279 protected function doUnlink(string $file): bool
280 {
281 unset(self::$valuesCache[$file]);
282
283 if (self::isSupported()) {
284 @opcache_invalidate($file, true);
285 }
286
287 return @unlink($file);
288 }
289
290 private function getFileKey(string $file): string
291 {
292 if (!$h = @fopen($file, 'r')) {
293 return '';
294 }
295
296 $encodedKey = substr(fgets($h), 8);
297 fclose($h);
298
299 return rawurldecode(rtrim($encodedKey));
300 }
301}
302
303/**
304 * @internal
305 */
306class LazyValue
307{
308 public string $file;
309
310 public function __construct(string $file)
311 {
312 $this->file = $file;
313 }
314}
diff --git a/vendor/symfony/cache/Adapter/ProxyAdapter.php b/vendor/symfony/cache/Adapter/ProxyAdapter.php
new file mode 100644
index 0000000..c022dd5
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/ProxyAdapter.php
@@ -0,0 +1,206 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Psr\Cache\CacheItemPoolInterface;
16use Symfony\Component\Cache\CacheItem;
17use Symfony\Component\Cache\PruneableInterface;
18use Symfony\Component\Cache\ResettableInterface;
19use Symfony\Component\Cache\Traits\ContractsTrait;
20use Symfony\Component\Cache\Traits\ProxyTrait;
21use Symfony\Contracts\Cache\CacheInterface;
22
23/**
24 * @author Nicolas Grekas <p@tchwork.com>
25 */
26class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
27{
28 use ContractsTrait;
29 use ProxyTrait;
30
31 private string $namespace = '';
32 private int $namespaceLen;
33 private string $poolHash;
34 private int $defaultLifetime;
35
36 private static \Closure $createCacheItem;
37 private static \Closure $setInnerItem;
38
39 public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
40 {
41 $this->pool = $pool;
42 $this->poolHash = spl_object_hash($pool);
43 if ('' !== $namespace) {
44 \assert('' !== CacheItem::validateKey($namespace));
45 $this->namespace = $namespace;
46 }
47 $this->namespaceLen = \strlen($namespace);
48 $this->defaultLifetime = $defaultLifetime;
49 self::$createCacheItem ??= \Closure::bind(
50 static function ($key, $innerItem, $poolHash) {
51 $item = new CacheItem();
52 $item->key = $key;
53
54 if (null === $innerItem) {
55 return $item;
56 }
57
58 $item->value = $innerItem->get();
59 $item->isHit = $innerItem->isHit();
60 $item->innerItem = $innerItem;
61 $item->poolHash = $poolHash;
62
63 if (!$item->unpack() && $innerItem instanceof CacheItem) {
64 $item->metadata = $innerItem->metadata;
65 }
66 $innerItem->set(null);
67
68 return $item;
69 },
70 null,
71 CacheItem::class
72 );
73 self::$setInnerItem ??= \Closure::bind(
74 static function (CacheItemInterface $innerItem, CacheItem $item, $expiry = null) {
75 $innerItem->set($item->pack());
76 $innerItem->expiresAt(($expiry ?? $item->expiry) ? \DateTimeImmutable::createFromFormat('U.u', sprintf('%.6F', $expiry ?? $item->expiry)) : null);
77 },
78 null,
79 CacheItem::class
80 );
81 }
82
83 public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
84 {
85 if (!$this->pool instanceof CacheInterface) {
86 return $this->doGet($this, $key, $callback, $beta, $metadata);
87 }
88
89 return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) {
90 $item = (self::$createCacheItem)($key, $innerItem, $this->poolHash);
91 $item->set($value = $callback($item, $save));
92 (self::$setInnerItem)($innerItem, $item);
93
94 return $value;
95 }, $beta, $metadata);
96 }
97
98 public function getItem(mixed $key): CacheItem
99 {
100 $item = $this->pool->getItem($this->getId($key));
101
102 return (self::$createCacheItem)($key, $item, $this->poolHash);
103 }
104
105 public function getItems(array $keys = []): iterable
106 {
107 if ($this->namespaceLen) {
108 foreach ($keys as $i => $key) {
109 $keys[$i] = $this->getId($key);
110 }
111 }
112
113 return $this->generateItems($this->pool->getItems($keys));
114 }
115
116 public function hasItem(mixed $key): bool
117 {
118 return $this->pool->hasItem($this->getId($key));
119 }
120
121 public function clear(string $prefix = ''): bool
122 {
123 if ($this->pool instanceof AdapterInterface) {
124 return $this->pool->clear($this->namespace.$prefix);
125 }
126
127 return $this->pool->clear();
128 }
129
130 public function deleteItem(mixed $key): bool
131 {
132 return $this->pool->deleteItem($this->getId($key));
133 }
134
135 public function deleteItems(array $keys): bool
136 {
137 if ($this->namespaceLen) {
138 foreach ($keys as $i => $key) {
139 $keys[$i] = $this->getId($key);
140 }
141 }
142
143 return $this->pool->deleteItems($keys);
144 }
145
146 public function save(CacheItemInterface $item): bool
147 {
148 return $this->doSave($item, __FUNCTION__);
149 }
150
151 public function saveDeferred(CacheItemInterface $item): bool
152 {
153 return $this->doSave($item, __FUNCTION__);
154 }
155
156 public function commit(): bool
157 {
158 return $this->pool->commit();
159 }
160
161 private function doSave(CacheItemInterface $item, string $method): bool
162 {
163 if (!$item instanceof CacheItem) {
164 return false;
165 }
166 $castItem = (array) $item;
167
168 if (null === $castItem["\0*\0expiry"] && 0 < $this->defaultLifetime) {
169 $castItem["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
170 }
171
172 if ($castItem["\0*\0poolHash"] === $this->poolHash && $castItem["\0*\0innerItem"]) {
173 $innerItem = $castItem["\0*\0innerItem"];
174 } elseif ($this->pool instanceof AdapterInterface) {
175 // this is an optimization specific for AdapterInterface implementations
176 // so we can save a round-trip to the backend by just creating a new item
177 $innerItem = (self::$createCacheItem)($this->namespace.$castItem["\0*\0key"], null, $this->poolHash);
178 } else {
179 $innerItem = $this->pool->getItem($this->namespace.$castItem["\0*\0key"]);
180 }
181
182 (self::$setInnerItem)($innerItem, $item, $castItem["\0*\0expiry"]);
183
184 return $this->pool->$method($innerItem);
185 }
186
187 private function generateItems(iterable $items): \Generator
188 {
189 $f = self::$createCacheItem;
190
191 foreach ($items as $key => $item) {
192 if ($this->namespaceLen) {
193 $key = substr($key, $this->namespaceLen);
194 }
195
196 yield $key => $f($key, $item, $this->poolHash);
197 }
198 }
199
200 private function getId(mixed $key): string
201 {
202 \assert('' !== CacheItem::validateKey($key));
203
204 return $this->namespace.$key;
205 }
206}
diff --git a/vendor/symfony/cache/Adapter/Psr16Adapter.php b/vendor/symfony/cache/Adapter/Psr16Adapter.php
new file mode 100644
index 0000000..a72b037
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/Psr16Adapter.php
@@ -0,0 +1,71 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\SimpleCache\CacheInterface;
15use Symfony\Component\Cache\PruneableInterface;
16use Symfony\Component\Cache\ResettableInterface;
17use Symfony\Component\Cache\Traits\ProxyTrait;
18
19/**
20 * Turns a PSR-16 cache into a PSR-6 one.
21 *
22 * @author Nicolas Grekas <p@tchwork.com>
23 */
24class 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}
diff --git a/vendor/symfony/cache/Adapter/RedisAdapter.php b/vendor/symfony/cache/Adapter/RedisAdapter.php
new file mode 100644
index 0000000..e33f2f6
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/RedisAdapter.php
@@ -0,0 +1,25 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Component\Cache\Marshaller\MarshallerInterface;
15use Symfony\Component\Cache\Traits\RedisTrait;
16
17class RedisAdapter extends AbstractAdapter
18{
19 use RedisTrait;
20
21 public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
22 {
23 $this->init($redis, $namespace, $defaultLifetime, $marshaller);
24 }
25}
diff --git a/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php b/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php
new file mode 100644
index 0000000..f71622b
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php
@@ -0,0 +1,310 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Predis\Connection\Aggregate\ClusterInterface;
15use Predis\Connection\Aggregate\PredisCluster;
16use Predis\Connection\Aggregate\ReplicationInterface;
17use Predis\Response\ErrorInterface;
18use Predis\Response\Status;
19use Relay\Relay;
20use Symfony\Component\Cache\CacheItem;
21use Symfony\Component\Cache\Exception\InvalidArgumentException;
22use Symfony\Component\Cache\Exception\LogicException;
23use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
24use Symfony\Component\Cache\Marshaller\MarshallerInterface;
25use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
26use 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 <p@tchwork.com>
45 * @author André Rømcke <andre.romcke+symfony@gmail.com>
46 */
47class RedisTagAwareAdapter extends AbstractTagAwareAdapter
48{
49 use RedisTrait;
50
51 /**
52 * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are
53 * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements.
54 */
55 private const DEFAULT_CACHE_TTL = 8640000;
56
57 /**
58 * detected eviction policy used on Redis server.
59 */
60 private string $redisEvictionPolicy;
61
62 public function __construct(
63 \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis,
64 private string $namespace = '',
65 int $defaultLifetime = 0,
66 ?MarshallerInterface $marshaller = null,
67 ) {
68 if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) {
69 throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
70 }
71
72 $isRelay = $redis instanceof Relay;
73 if ($isRelay || \defined('Redis::OPT_COMPRESSION') && \in_array($redis::class, [\Redis::class, \RedisArray::class, \RedisCluster::class], true)) {
74 $compression = $redis->getOption($isRelay ? Relay::OPT_COMPRESSION : \Redis::OPT_COMPRESSION);
75
76 foreach (\is_array($compression) ? $compression : [$compression] as $c) {
77 if ($isRelay ? Relay::COMPRESSION_NONE : \Redis::COMPRESSION_NONE !== $c) {
78 throw new InvalidArgumentException(sprintf('redis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class));
79 }
80 }
81 }
82
83 $this->init($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller));
84 }
85
86 protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array
87 {
88 $eviction = $this->getRedisEvictionPolicy();
89 if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) {
90 throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction));
91 }
92
93 // serialize values
94 if (!$serialized = $this->marshaller->marshall($values, $failed)) {
95 return $failed;
96 }
97
98 // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op
99 $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) {
100 // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one
101 foreach ($serialized as $id => $value) {
102 yield 'setEx' => [
103 $id,
104 0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime,
105 $value,
106 ];
107 }
108
109 // Add and Remove Tags
110 foreach ($addTagData as $tagId => $ids) {
111 if (!$failed || $ids = array_diff($ids, $failed)) {
112 yield 'sAdd' => array_merge([$tagId], $ids);
113 }
114 }
115
116 foreach ($delTagData as $tagId => $ids) {
117 if (!$failed || $ids = array_diff($ids, $failed)) {
118 yield 'sRem' => array_merge([$tagId], $ids);
119 }
120 }
121 });
122
123 foreach ($results as $id => $result) {
124 // Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not
125 if (is_numeric($result)) {
126 continue;
127 }
128 // setEx results
129 if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
130 $failed[] = $id;
131 }
132 }
133
134 return $failed;
135 }
136
137 protected function doDeleteYieldTags(array $ids): iterable
138 {
139 $lua = <<<'EOLUA'
140 local v = redis.call('GET', KEYS[1])
141 local e = redis.pcall('UNLINK', KEYS[1])
142
143 if type(e) ~= 'number' then
144 redis.call('DEL', KEYS[1])
145 end
146
147 if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then
148 return ''
149 end
150
151 return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536)
152EOLUA;
153
154 $results = $this->pipeline(function () use ($ids, $lua) {
155 foreach ($ids as $id) {
156 yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1];
157 }
158 });
159
160 foreach ($results as $id => $result) {
161 if ($result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) {
162 CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);
163
164 continue;
165 }
166
167 try {
168 yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
169 } catch (\Exception) {
170 yield $id => [];
171 }
172 }
173 }
174
175 protected function doDeleteTagRelations(array $tagData): bool
176 {
177 $results = $this->pipeline(static function () use ($tagData) {
178 foreach ($tagData as $tagId => $idList) {
179 array_unshift($idList, $tagId);
180 yield 'sRem' => $idList;
181 }
182 });
183 foreach ($results as $result) {
184 // no-op
185 }
186
187 return true;
188 }
189
190 protected function doInvalidate(array $tagIds): bool
191 {
192 // This script scans the set of items linked to tag: it empties the set
193 // and removes the linked items. When the set is still not empty after
194 // the scan, it means we're in cluster mode and that the linked items
195 // are on other nodes: we move the links to a temporary set and we
196 // garbage collect that set from the client side.
197
198 $lua = <<<'EOLUA'
199 redis.replicate_commands()
200
201 local cursor = '0'
202 local id = KEYS[1]
203 repeat
204 local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000);
205 cursor = result[1];
206 local rems = {}
207
208 for _, v in ipairs(result[2]) do
209 local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v)
210 if ok then
211 table.insert(rems, v)
212 end
213 end
214 if 0 < #rems then
215 redis.call('SREM', id, unpack(rems))
216 end
217 until '0' == cursor;
218
219 redis.call('SUNIONSTORE', '{'..id..'}'..id, id)
220 redis.call('DEL', id)
221
222 return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000)
223EOLUA;
224
225 $results = $this->pipeline(function () use ($tagIds, $lua) {
226 if ($this->redis instanceof \Predis\ClientInterface) {
227 $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
228 } elseif (\is_array($prefix = $this->redis->getOption($this->redis instanceof Relay ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) {
229 $prefix = current($prefix);
230 }
231
232 foreach ($tagIds as $id) {
233 yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1];
234 }
235 });
236
237 $lua = <<<'EOLUA'
238 redis.replicate_commands()
239
240 local id = KEYS[1]
241 local cursor = table.remove(ARGV)
242 redis.call('SREM', '{'..id..'}'..id, unpack(ARGV))
243
244 return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000)
245EOLUA;
246
247 $success = true;
248 foreach ($results as $id => $values) {
249 if ($values instanceof \RedisException || $values instanceof \Relay\Exception || $values instanceof ErrorInterface) {
250 CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]);
251 $success = false;
252
253 continue;
254 }
255
256 [$cursor, $ids] = $values;
257
258 while ($ids || '0' !== $cursor) {
259 $this->doDelete($ids);
260
261 $evalArgs = [$id, $cursor];
262 array_splice($evalArgs, 1, 0, $ids);
263
264 if ($this->redis instanceof \Predis\ClientInterface) {
265 array_unshift($evalArgs, $lua, 1);
266 } else {
267 $evalArgs = [$lua, $evalArgs, 1];
268 }
269
270 $results = $this->pipeline(function () use ($evalArgs) {
271 yield 'eval' => $evalArgs;
272 });
273
274 foreach ($results as [$cursor, $ids]) {
275 // no-op
276 }
277 }
278 }
279
280 return $success;
281 }
282
283 private function getRedisEvictionPolicy(): string
284 {
285 if (isset($this->redisEvictionPolicy)) {
286 return $this->redisEvictionPolicy;
287 }
288
289 $hosts = $this->getHosts();
290 $host = reset($hosts);
291 if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
292 // Predis supports info command only on the master in replication environments
293 $hosts = [$host->getClientFor('master')];
294 }
295
296 foreach ($hosts as $host) {
297 $info = $host->info('Memory');
298
299 if (false === $info || null === $info || $info instanceof ErrorInterface) {
300 continue;
301 }
302
303 $info = $info['Memory'] ?? $info;
304
305 return $this->redisEvictionPolicy = $info['maxmemory_policy'] ?? '';
306 }
307
308 return $this->redisEvictionPolicy = '';
309 }
310}
diff --git a/vendor/symfony/cache/Adapter/TagAwareAdapter.php b/vendor/symfony/cache/Adapter/TagAwareAdapter.php
new file mode 100644
index 0000000..34082db
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/TagAwareAdapter.php
@@ -0,0 +1,370 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Psr\Cache\InvalidArgumentException;
16use Psr\Log\LoggerAwareInterface;
17use Psr\Log\LoggerAwareTrait;
18use Symfony\Component\Cache\CacheItem;
19use Symfony\Component\Cache\PruneableInterface;
20use Symfony\Component\Cache\ResettableInterface;
21use Symfony\Component\Cache\Traits\ContractsTrait;
22use Symfony\Contracts\Cache\TagAwareCacheInterface;
23
24/**
25 * Implements simple and robust tag-based invalidation suitable for use with volatile caches.
26 *
27 * This adapter works by storing a version for each tags. When saving an item, it is stored together with its tags and
28 * their corresponding versions. When retrieving an item, those tag versions are compared to the current version of
29 * each tags. Invalidation is achieved by deleting tags, thereby ensuring that their versions change even when the
30 * storage is out of space. When versions of non-existing tags are requested for item commits, this adapter assigns a
31 * new random version to them.
32 *
33 * @author Nicolas Grekas <p@tchwork.com>
34 * @author Sergey Belyshkin <sbelyshkin@gmail.com>
35 */
36class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface
37{
38 use ContractsTrait;
39 use LoggerAwareTrait;
40
41 public const TAGS_PREFIX = "\1tags\1";
42
43 private array $deferred = [];
44 private AdapterInterface $pool;
45 private AdapterInterface $tags;
46 private array $knownTagVersions = [];
47
48 private static \Closure $setCacheItemTags;
49 private static \Closure $setTagVersions;
50 private static \Closure $getTagsByKey;
51 private static \Closure $saveTags;
52
53 public function __construct(
54 AdapterInterface $itemsPool,
55 ?AdapterInterface $tagsPool = null,
56 private float $knownTagVersionsTtl = 0.15,
57 ) {
58 $this->pool = $itemsPool;
59 $this->tags = $tagsPool ?? $itemsPool;
60 self::$setCacheItemTags ??= \Closure::bind(
61 static function (array $items, array $itemTags) {
62 foreach ($items as $key => $item) {
63 $item->isTaggable = true;
64
65 if (isset($itemTags[$key])) {
66 $tags = array_keys($itemTags[$key]);
67 $item->metadata[CacheItem::METADATA_TAGS] = array_combine($tags, $tags);
68 } else {
69 $item->value = null;
70 $item->isHit = false;
71 $item->metadata = [];
72 }
73 }
74
75 return $items;
76 },
77 null,
78 CacheItem::class
79 );
80 self::$setTagVersions ??= \Closure::bind(
81 static function (array $items, array $tagVersions) {
82 foreach ($items as $item) {
83 $item->newMetadata[CacheItem::METADATA_TAGS] = array_intersect_key($tagVersions, $item->newMetadata[CacheItem::METADATA_TAGS] ?? []);
84 }
85 },
86 null,
87 CacheItem::class
88 );
89 self::$getTagsByKey ??= \Closure::bind(
90 static function ($deferred) {
91 $tagsByKey = [];
92 foreach ($deferred as $key => $item) {
93 $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
94 $item->metadata = $item->newMetadata;
95 }
96
97 return $tagsByKey;
98 },
99 null,
100 CacheItem::class
101 );
102 self::$saveTags ??= \Closure::bind(
103 static function (AdapterInterface $tagsAdapter, array $tags) {
104 ksort($tags);
105
106 foreach ($tags as $v) {
107 $v->expiry = 0;
108 $tagsAdapter->saveDeferred($v);
109 }
110
111 return $tagsAdapter->commit();
112 },
113 null,
114 CacheItem::class
115 );
116 }
117
118 public function invalidateTags(array $tags): bool
119 {
120 $ids = [];
121 foreach ($tags as $tag) {
122 \assert('' !== CacheItem::validateKey($tag));
123 unset($this->knownTagVersions[$tag]);
124 $ids[] = $tag.static::TAGS_PREFIX;
125 }
126
127 return !$tags || $this->tags->deleteItems($ids);
128 }
129
130 public function hasItem(mixed $key): bool
131 {
132 return $this->getItem($key)->isHit();
133 }
134
135 public function getItem(mixed $key): CacheItem
136 {
137 foreach ($this->getItems([$key]) as $item) {
138 return $item;
139 }
140 }
141
142 public function getItems(array $keys = []): iterable
143 {
144 $tagKeys = [];
145 $commit = false;
146
147 foreach ($keys as $key) {
148 if ('' !== $key && \is_string($key)) {
149 $commit = $commit || isset($this->deferred[$key]);
150 }
151 }
152
153 if ($commit) {
154 $this->commit();
155 }
156
157 try {
158 $items = $this->pool->getItems($keys);
159 } catch (InvalidArgumentException $e) {
160 $this->pool->getItems($keys); // Should throw an exception
161
162 throw $e;
163 }
164
165 $bufferedItems = $itemTags = [];
166
167 foreach ($items as $key => $item) {
168 if (null !== $tags = $item->getMetadata()[CacheItem::METADATA_TAGS] ?? null) {
169 $itemTags[$key] = $tags;
170 }
171
172 $bufferedItems[$key] = $item;
173
174 if (null === $tags) {
175 $key = "\0tags\0".$key;
176 $tagKeys[$key] = $key; // BC with pools populated before v6.1
177 }
178 }
179
180 if ($tagKeys) {
181 foreach ($this->pool->getItems($tagKeys) as $key => $item) {
182 if ($item->isHit()) {
183 $itemTags[substr($key, \strlen("\0tags\0"))] = $item->get() ?: [];
184 }
185 }
186 }
187
188 $tagVersions = $this->getTagVersions($itemTags, false);
189 foreach ($itemTags as $key => $tags) {
190 foreach ($tags as $tag => $version) {
191 if ($tagVersions[$tag] !== $version) {
192 unset($itemTags[$key]);
193 continue 2;
194 }
195 }
196 }
197 $tagVersions = null;
198
199 return (self::$setCacheItemTags)($bufferedItems, $itemTags);
200 }
201
202 public function clear(string $prefix = ''): bool
203 {
204 if ('' !== $prefix) {
205 foreach ($this->deferred as $key => $item) {
206 if (str_starts_with($key, $prefix)) {
207 unset($this->deferred[$key]);
208 }
209 }
210 } else {
211 $this->deferred = [];
212 }
213
214 if ($this->pool instanceof AdapterInterface) {
215 return $this->pool->clear($prefix);
216 }
217
218 return $this->pool->clear();
219 }
220
221 public function deleteItem(mixed $key): bool
222 {
223 return $this->deleteItems([$key]);
224 }
225
226 public function deleteItems(array $keys): bool
227 {
228 foreach ($keys as $key) {
229 if ('' !== $key && \is_string($key)) {
230 $keys[] = "\0tags\0".$key; // BC with pools populated before v6.1
231 }
232 }
233
234 return $this->pool->deleteItems($keys);
235 }
236
237 public function save(CacheItemInterface $item): bool
238 {
239 if (!$item instanceof CacheItem) {
240 return false;
241 }
242 $this->deferred[$item->getKey()] = $item;
243
244 return $this->commit();
245 }
246
247 public function saveDeferred(CacheItemInterface $item): bool
248 {
249 if (!$item instanceof CacheItem) {
250 return false;
251 }
252 $this->deferred[$item->getKey()] = $item;
253
254 return true;
255 }
256
257 public function commit(): bool
258 {
259 if (!$items = $this->deferred) {
260 return true;
261 }
262
263 $tagVersions = $this->getTagVersions((self::$getTagsByKey)($items), true);
264 (self::$setTagVersions)($items, $tagVersions);
265
266 $ok = true;
267 foreach ($items as $key => $item) {
268 if ($this->pool->saveDeferred($item)) {
269 unset($this->deferred[$key]);
270 } else {
271 $ok = false;
272 }
273 }
274 $ok = $this->pool->commit() && $ok;
275
276 $tagVersions = array_keys($tagVersions);
277 (self::$setTagVersions)($items, array_combine($tagVersions, $tagVersions));
278
279 return $ok;
280 }
281
282 public function prune(): bool
283 {
284 return $this->pool instanceof PruneableInterface && $this->pool->prune();
285 }
286
287 public function reset(): void
288 {
289 $this->commit();
290 $this->knownTagVersions = [];
291 $this->pool instanceof ResettableInterface && $this->pool->reset();
292 $this->tags instanceof ResettableInterface && $this->tags->reset();
293 }
294
295 public function __sleep(): array
296 {
297 throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
298 }
299
300 public function __wakeup(): void
301 {
302 throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
303 }
304
305 public function __destruct()
306 {
307 $this->commit();
308 }
309
310 private function getTagVersions(array $tagsByKey, bool $persistTags): array
311 {
312 $tagVersions = [];
313 $fetchTagVersions = $persistTags;
314
315 foreach ($tagsByKey as $tags) {
316 $tagVersions += $tags;
317 if ($fetchTagVersions) {
318 continue;
319 }
320 foreach ($tags as $tag => $version) {
321 if ($tagVersions[$tag] !== $version) {
322 $fetchTagVersions = true;
323 }
324 }
325 }
326
327 if (!$tagVersions) {
328 return [];
329 }
330
331 $now = microtime(true);
332 $tags = [];
333 foreach ($tagVersions as $tag => $version) {
334 $tags[$tag.static::TAGS_PREFIX] = $tag;
335 $knownTagVersion = $this->knownTagVersions[$tag] ?? [0, null];
336 if ($fetchTagVersions || $now > $knownTagVersion[0] || $knownTagVersion[1] !== $version) {
337 // reuse previously fetched tag versions until the expiration
338 $fetchTagVersions = true;
339 }
340 }
341
342 if (!$fetchTagVersions) {
343 return $tagVersions;
344 }
345
346 $newTags = [];
347 $newVersion = null;
348 $expiration = $now + $this->knownTagVersionsTtl;
349 foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
350 unset($this->knownTagVersions[$tag = $tags[$tag]]); // update FIFO
351 if (null !== $tagVersions[$tag] = $version->get()) {
352 $this->knownTagVersions[$tag] = [$expiration, $tagVersions[$tag]];
353 } elseif ($persistTags) {
354 $newTags[$tag] = $version->set($newVersion ??= random_bytes(6));
355 $tagVersions[$tag] = $newVersion;
356 $this->knownTagVersions[$tag] = [$expiration, $newVersion];
357 }
358 }
359
360 if ($newTags) {
361 (self::$saveTags)($this->tags, $newTags);
362 }
363
364 while ($now > ($this->knownTagVersions[$tag = array_key_first($this->knownTagVersions)][0] ?? \INF)) {
365 unset($this->knownTagVersions[$tag]);
366 }
367
368 return $tagVersions;
369 }
370}
diff --git a/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php b/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php
new file mode 100644
index 0000000..9242779
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php
@@ -0,0 +1,31 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\InvalidArgumentException;
15
16/**
17 * Interface for invalidating cached items using tags.
18 *
19 * @author Nicolas Grekas <p@tchwork.com>
20 */
21interface 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}
diff --git a/vendor/symfony/cache/Adapter/TraceableAdapter.php b/vendor/symfony/cache/Adapter/TraceableAdapter.php
new file mode 100644
index 0000000..b5bce14
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/TraceableAdapter.php
@@ -0,0 +1,250 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Psr\Cache\CacheItemInterface;
15use Symfony\Component\Cache\CacheItem;
16use Symfony\Component\Cache\PruneableInterface;
17use Symfony\Component\Cache\ResettableInterface;
18use Symfony\Contracts\Cache\CacheInterface;
19use Symfony\Contracts\Service\ResetInterface;
20
21/**
22 * An adapter that collects data about all cache calls.
23 *
24 * @author Aaron Scherer <aequasi@gmail.com>
25 * @author Tobias Nyholm <tobias.nyholm@gmail.com>
26 * @author Nicolas Grekas <p@tchwork.com>
27 */
28class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
29{
30 protected AdapterInterface $pool;
31 private array $calls = [];
32
33 public function __construct(AdapterInterface $pool)
34 {
35 $this->pool = $pool;
36 }
37
38 public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed
39 {
40 if (!$this->pool instanceof CacheInterface) {
41 throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class));
42 }
43
44 $isHit = true;
45 $callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) {
46 $isHit = $item->isHit();
47
48 return $callback($item, $save);
49 };
50
51 $event = $this->start(__FUNCTION__);
52 try {
53 $value = $this->pool->get($key, $callback, $beta, $metadata);
54 $event->result[$key] = get_debug_type($value);
55 } finally {
56 $event->end = microtime(true);
57 }
58 if ($isHit) {
59 ++$event->hits;
60 } else {
61 ++$event->misses;
62 }
63
64 return $value;
65 }
66
67 public function getItem(mixed $key): CacheItem
68 {
69 $event = $this->start(__FUNCTION__);
70 try {
71 $item = $this->pool->getItem($key);
72 } finally {
73 $event->end = microtime(true);
74 }
75 if ($event->result[$key] = $item->isHit()) {
76 ++$event->hits;
77 } else {
78 ++$event->misses;
79 }
80
81 return $item;
82 }
83
84 public function hasItem(mixed $key): bool
85 {
86 $event = $this->start(__FUNCTION__);
87 try {
88 return $event->result[$key] = $this->pool->hasItem($key);
89 } finally {
90 $event->end = microtime(true);
91 }
92 }
93
94 public function deleteItem(mixed $key): bool
95 {
96 $event = $this->start(__FUNCTION__);
97 try {
98 return $event->result[$key] = $this->pool->deleteItem($key);
99 } finally {
100 $event->end = microtime(true);
101 }
102 }
103
104 public function save(CacheItemInterface $item): bool
105 {
106 $event = $this->start(__FUNCTION__);
107 try {
108 return $event->result[$item->getKey()] = $this->pool->save($item);
109 } finally {
110 $event->end = microtime(true);
111 }
112 }
113
114 public function saveDeferred(CacheItemInterface $item): bool
115 {
116 $event = $this->start(__FUNCTION__);
117 try {
118 return $event->result[$item->getKey()] = $this->pool->saveDeferred($item);
119 } finally {
120 $event->end = microtime(true);
121 }
122 }
123
124 public function getItems(array $keys = []): iterable
125 {
126 $event = $this->start(__FUNCTION__);
127 try {
128 $result = $this->pool->getItems($keys);
129 } finally {
130 $event->end = microtime(true);
131 }
132 $f = function () use ($result, $event) {
133 $event->result = [];
134 foreach ($result as $key => $item) {
135 if ($event->result[$key] = $item->isHit()) {
136 ++$event->hits;
137 } else {
138 ++$event->misses;
139 }
140 yield $key => $item;
141 }
142 };
143
144 return $f();
145 }
146
147 public function clear(string $prefix = ''): bool
148 {
149 $event = $this->start(__FUNCTION__);
150 try {
151 if ($this->pool instanceof AdapterInterface) {
152 return $event->result = $this->pool->clear($prefix);
153 }
154
155 return $event->result = $this->pool->clear();
156 } finally {
157 $event->end = microtime(true);
158 }
159 }
160
161 public function deleteItems(array $keys): bool
162 {
163 $event = $this->start(__FUNCTION__);
164 $event->result['keys'] = $keys;
165 try {
166 return $event->result['result'] = $this->pool->deleteItems($keys);
167 } finally {
168 $event->end = microtime(true);
169 }
170 }
171
172 public function commit(): bool
173 {
174 $event = $this->start(__FUNCTION__);
175 try {
176 return $event->result = $this->pool->commit();
177 } finally {
178 $event->end = microtime(true);
179 }
180 }
181
182 public function prune(): bool
183 {
184 if (!$this->pool instanceof PruneableInterface) {
185 return false;
186 }
187 $event = $this->start(__FUNCTION__);
188 try {
189 return $event->result = $this->pool->prune();
190 } finally {
191 $event->end = microtime(true);
192 }
193 }
194
195 public function reset(): void
196 {
197 if ($this->pool instanceof ResetInterface) {
198 $this->pool->reset();
199 }
200
201 $this->clearCalls();
202 }
203
204 public function delete(string $key): bool
205 {
206 $event = $this->start(__FUNCTION__);
207 try {
208 return $event->result[$key] = $this->pool->deleteItem($key);
209 } finally {
210 $event->end = microtime(true);
211 }
212 }
213
214 public function getCalls(): array
215 {
216 return $this->calls;
217 }
218
219 public function clearCalls(): void
220 {
221 $this->calls = [];
222 }
223
224 public function getPool(): AdapterInterface
225 {
226 return $this->pool;
227 }
228
229 protected function start(string $name): TraceableAdapterEvent
230 {
231 $this->calls[] = $event = new TraceableAdapterEvent();
232 $event->name = $name;
233 $event->start = microtime(true);
234
235 return $event;
236 }
237}
238
239/**
240 * @internal
241 */
242class TraceableAdapterEvent
243{
244 public string $name;
245 public float $start;
246 public float $end;
247 public array|bool $result;
248 public int $hits = 0;
249 public int $misses = 0;
250}
diff --git a/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php b/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php
new file mode 100644
index 0000000..c85d199
--- /dev/null
+++ b/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php
@@ -0,0 +1,35 @@
1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Cache\Adapter;
13
14use Symfony\Contracts\Cache\TagAwareCacheInterface;
15
16/**
17 * @author Robin Chalas <robin.chalas@gmail.com>
18 */
19class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface
20{
21 public function __construct(TagAwareAdapterInterface $pool)
22 {
23 parent::__construct($pool);
24 }
25
26 public function invalidateTags(array $tags): bool
27 {
28 $event = $this->start(__FUNCTION__);
29 try {
30 return $event->result = $this->pool->invalidateTags($tags);
31 } finally {
32 $event->end = microtime(true);
33 }
34 }
35}