diff options
author | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
---|---|---|
committer | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
commit | bf6655a534a6775d30cafa67bd801276bda1d98d (patch) | |
tree | c6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/symfony/cache/Adapter | |
parent | 94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff) | |
download | AppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip |
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/symfony/cache/Adapter')
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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Log\LoggerAwareInterface; | ||
15 | use Psr\Log\LoggerInterface; | ||
16 | use Symfony\Component\Cache\CacheItem; | ||
17 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
18 | use Symfony\Component\Cache\ResettableInterface; | ||
19 | use Symfony\Component\Cache\Traits\AbstractAdapterTrait; | ||
20 | use Symfony\Component\Cache\Traits\ContractsTrait; | ||
21 | use Symfony\Contracts\Cache\CacheInterface; | ||
22 | |||
23 | /** | ||
24 | * @author Nicolas Grekas <p@tchwork.com> | ||
25 | */ | ||
26 | abstract 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Log\LoggerAwareInterface; | ||
15 | use Symfony\Component\Cache\CacheItem; | ||
16 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
17 | use Symfony\Component\Cache\ResettableInterface; | ||
18 | use Symfony\Component\Cache\Traits\AbstractAdapterTrait; | ||
19 | use Symfony\Component\Cache\Traits\ContractsTrait; | ||
20 | use 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 | */ | ||
33 | abstract 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemPoolInterface; | ||
15 | use Symfony\Component\Cache\CacheItem; | ||
16 | |||
17 | // Help opcache.preload discover always-needed symbols | ||
18 | class_exists(CacheItem::class); | ||
19 | |||
20 | /** | ||
21 | * Interface for adapters managing instances of Symfony's CacheItem. | ||
22 | * | ||
23 | * @author Kévin Dunglas <dunglas@gmail.com> | ||
24 | */ | ||
25 | interface 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\CacheItem; | ||
15 | use Symfony\Component\Cache\Exception\CacheException; | ||
16 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
17 | |||
18 | /** | ||
19 | * @author Nicolas Grekas <p@tchwork.com> | ||
20 | */ | ||
21 | class ApcuAdapter extends AbstractAdapter | ||
22 | { | ||
23 | /** | ||
24 | * @throws CacheException if APCu is not enabled | ||
25 | */ | ||
26 | public function __construct( | ||
27 | string $namespace = '', | ||
28 | int $defaultLifetime = 0, | ||
29 | ?string $version = null, | ||
30 | private ?MarshallerInterface $marshaller = null, | ||
31 | ) { | ||
32 | if (!static::isSupported()) { | ||
33 | throw new CacheException('APCu is not enabled.'); | ||
34 | } | ||
35 | if ('cli' === \PHP_SAPI) { | ||
36 | ini_set('apc.use_request_time', 0); | ||
37 | } | ||
38 | parent::__construct($namespace, $defaultLifetime); | ||
39 | |||
40 | if (null !== $version) { | ||
41 | CacheItem::validateKey($version); | ||
42 | |||
43 | if (!apcu_exists($version.'@'.$namespace)) { | ||
44 | $this->doClear($namespace); | ||
45 | apcu_add($version.'@'.$namespace, null); | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | |||
50 | public static function isSupported(): bool | ||
51 | { | ||
52 | return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL); | ||
53 | } | ||
54 | |||
55 | protected function doFetch(array $ids): iterable | ||
56 | { | ||
57 | $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); | ||
58 | try { | ||
59 | $values = []; | ||
60 | foreach (apcu_fetch($ids, $ok) ?: [] as $k => $v) { | ||
61 | if (null !== $v || $ok) { | ||
62 | $values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v; | ||
63 | } | ||
64 | } | ||
65 | |||
66 | return $values; | ||
67 | } catch (\Error $e) { | ||
68 | throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); | ||
69 | } finally { | ||
70 | ini_set('unserialize_callback_func', $unserializeCallbackHandler); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | protected function doHave(string $id): bool | ||
75 | { | ||
76 | return apcu_exists($id); | ||
77 | } | ||
78 | |||
79 | protected function doClear(string $namespace): bool | ||
80 | { | ||
81 | return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) | ||
82 | ? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY)) | ||
83 | : apcu_clear_cache(); | ||
84 | } | ||
85 | |||
86 | protected function doDelete(array $ids): bool | ||
87 | { | ||
88 | foreach ($ids as $id) { | ||
89 | apcu_delete($id); | ||
90 | } | ||
91 | |||
92 | return true; | ||
93 | } | ||
94 | |||
95 | protected function doSave(array $values, int $lifetime): array|bool | ||
96 | { | ||
97 | if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) { | ||
98 | return $failed; | ||
99 | } | ||
100 | |||
101 | 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Psr\Log\LoggerAwareInterface; | ||
16 | use Psr\Log\LoggerAwareTrait; | ||
17 | use Symfony\Component\Cache\CacheItem; | ||
18 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
19 | use Symfony\Component\Cache\ResettableInterface; | ||
20 | use 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 | */ | ||
29 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Psr\Cache\CacheItemPoolInterface; | ||
16 | use Symfony\Component\Cache\CacheItem; | ||
17 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
18 | use Symfony\Component\Cache\PruneableInterface; | ||
19 | use Symfony\Component\Cache\ResettableInterface; | ||
20 | use Symfony\Component\Cache\Traits\ContractsTrait; | ||
21 | use Symfony\Contracts\Cache\CacheInterface; | ||
22 | use 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 | */ | ||
32 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Exception\CacheException; | ||
15 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
16 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
17 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
18 | |||
19 | trigger_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 | */ | ||
26 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Couchbase\Bucket; | ||
15 | use Couchbase\Cluster; | ||
16 | use Couchbase\ClusterOptions; | ||
17 | use Couchbase\Collection; | ||
18 | use Couchbase\DocumentNotFoundException; | ||
19 | use Couchbase\UpsertOptions; | ||
20 | use Symfony\Component\Cache\Exception\CacheException; | ||
21 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
22 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
23 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
24 | |||
25 | /** | ||
26 | * @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com> | ||
27 | */ | ||
28 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Doctrine\DBAL\ArrayParameterType; | ||
15 | use Doctrine\DBAL\Configuration; | ||
16 | use Doctrine\DBAL\Connection; | ||
17 | use Doctrine\DBAL\DriverManager; | ||
18 | use Doctrine\DBAL\Exception as DBALException; | ||
19 | use Doctrine\DBAL\Exception\TableNotFoundException; | ||
20 | use Doctrine\DBAL\ParameterType; | ||
21 | use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; | ||
22 | use Doctrine\DBAL\Schema\Schema; | ||
23 | use Doctrine\DBAL\Tools\DsnParser; | ||
24 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
25 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
26 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
27 | use Symfony\Component\Cache\PruneableInterface; | ||
28 | |||
29 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
15 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
16 | use Symfony\Component\Cache\PruneableInterface; | ||
17 | use Symfony\Component\Cache\Traits\FilesystemTrait; | ||
18 | |||
19 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
15 | use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; | ||
16 | use Symfony\Component\Cache\PruneableInterface; | ||
17 | use 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 | */ | ||
25 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Exception\CacheException; | ||
15 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
16 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
17 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
18 | |||
19 | /** | ||
20 | * @author Rob Frawley 2nd <rmf@src.run> | ||
21 | * @author Nicolas Grekas <p@tchwork.com> | ||
22 | */ | ||
23 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Symfony\Component\Cache\CacheItem; | ||
16 | use Symfony\Contracts\Cache\CacheInterface; | ||
17 | |||
18 | /** | ||
19 | * @author Titouan Galopin <galopintitouan@gmail.com> | ||
20 | */ | ||
21 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | /** | ||
15 | * @author Lars Strojny <lars@strojny.net> | ||
16 | */ | ||
17 | final 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
15 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; | ||
16 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
17 | use Symfony\Component\Cache\PruneableInterface; | ||
18 | |||
19 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Psr\Cache\CacheItemPoolInterface; | ||
16 | use Symfony\Component\Cache\CacheItem; | ||
17 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
18 | use Symfony\Component\Cache\PruneableInterface; | ||
19 | use Symfony\Component\Cache\ResettableInterface; | ||
20 | use Symfony\Component\Cache\Traits\ContractsTrait; | ||
21 | use Symfony\Component\Cache\Traits\ProxyTrait; | ||
22 | use Symfony\Component\VarExporter\VarExporter; | ||
23 | use 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 | */ | ||
32 | class 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 | |||
279 | return [[ | ||
280 | |||
281 | |||
282 | EOF; | ||
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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Exception\CacheException; | ||
15 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
16 | use Symfony\Component\Cache\PruneableInterface; | ||
17 | use Symfony\Component\Cache\Traits\FilesystemCommonTrait; | ||
18 | use 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 | */ | ||
25 | class 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 | */ | ||
306 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Psr\Cache\CacheItemPoolInterface; | ||
16 | use Symfony\Component\Cache\CacheItem; | ||
17 | use Symfony\Component\Cache\PruneableInterface; | ||
18 | use Symfony\Component\Cache\ResettableInterface; | ||
19 | use Symfony\Component\Cache\Traits\ContractsTrait; | ||
20 | use Symfony\Component\Cache\Traits\ProxyTrait; | ||
21 | use Symfony\Contracts\Cache\CacheInterface; | ||
22 | |||
23 | /** | ||
24 | * @author Nicolas Grekas <p@tchwork.com> | ||
25 | */ | ||
26 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\SimpleCache\CacheInterface; | ||
15 | use Symfony\Component\Cache\PruneableInterface; | ||
16 | use Symfony\Component\Cache\ResettableInterface; | ||
17 | use Symfony\Component\Cache\Traits\ProxyTrait; | ||
18 | |||
19 | /** | ||
20 | * Turns a PSR-16 cache into a PSR-6 one. | ||
21 | * | ||
22 | * @author Nicolas Grekas <p@tchwork.com> | ||
23 | */ | ||
24 | class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface | ||
25 | { | ||
26 | use ProxyTrait; | ||
27 | |||
28 | /** | ||
29 | * @internal | ||
30 | */ | ||
31 | protected const NS_SEPARATOR = '_'; | ||
32 | |||
33 | private object $miss; | ||
34 | |||
35 | public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0) | ||
36 | { | ||
37 | parent::__construct($namespace, $defaultLifetime); | ||
38 | |||
39 | $this->pool = $pool; | ||
40 | $this->miss = new \stdClass(); | ||
41 | } | ||
42 | |||
43 | protected function doFetch(array $ids): iterable | ||
44 | { | ||
45 | foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) { | ||
46 | if ($this->miss !== $value) { | ||
47 | yield $key => $value; | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | protected function doHave(string $id): bool | ||
53 | { | ||
54 | return $this->pool->has($id); | ||
55 | } | ||
56 | |||
57 | protected function doClear(string $namespace): bool | ||
58 | { | ||
59 | return $this->pool->clear(); | ||
60 | } | ||
61 | |||
62 | protected function doDelete(array $ids): bool | ||
63 | { | ||
64 | return $this->pool->deleteMultiple($ids); | ||
65 | } | ||
66 | |||
67 | protected function doSave(array $values, int $lifetime): array|bool | ||
68 | { | ||
69 | return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); | ||
70 | } | ||
71 | } | ||
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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
15 | use Symfony\Component\Cache\Traits\RedisTrait; | ||
16 | |||
17 | class RedisAdapter extends AbstractAdapter | ||
18 | { | ||
19 | use RedisTrait; | ||
20 | |||
21 | public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay $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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Predis\Connection\Aggregate\ClusterInterface; | ||
15 | use Predis\Connection\Aggregate\PredisCluster; | ||
16 | use Predis\Connection\Aggregate\ReplicationInterface; | ||
17 | use Predis\Response\ErrorInterface; | ||
18 | use Predis\Response\Status; | ||
19 | use Relay\Relay; | ||
20 | use Symfony\Component\Cache\CacheItem; | ||
21 | use Symfony\Component\Cache\Exception\InvalidArgumentException; | ||
22 | use Symfony\Component\Cache\Exception\LogicException; | ||
23 | use Symfony\Component\Cache\Marshaller\DeflateMarshaller; | ||
24 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; | ||
25 | use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; | ||
26 | use Symfony\Component\Cache\Traits\RedisTrait; | ||
27 | |||
28 | /** | ||
29 | * Stores tag id <> cache id relationship as a Redis Set. | ||
30 | * | ||
31 | * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even | ||
32 | * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache | ||
33 | * relationship survives eviction (cache cleanup when Redis runs out of memory). | ||
34 | * | ||
35 | * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up | ||
36 | * | ||
37 | * Design limitations: | ||
38 | * - Max 4 billion cache keys per cache tag as limited by Redis Set datatype. | ||
39 | * E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also. | ||
40 | * | ||
41 | * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies. | ||
42 | * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype. | ||
43 | * | ||
44 | * @author Nicolas Grekas <p@tchwork.com> | ||
45 | * @author André Rømcke <andre.romcke+symfony@gmail.com> | ||
46 | */ | ||
47 | class 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) | ||
152 | EOLUA; | ||
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) | ||
223 | EOLUA; | ||
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) | ||
245 | EOLUA; | ||
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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Psr\Cache\InvalidArgumentException; | ||
16 | use Psr\Log\LoggerAwareInterface; | ||
17 | use Psr\Log\LoggerAwareTrait; | ||
18 | use Symfony\Component\Cache\CacheItem; | ||
19 | use Symfony\Component\Cache\PruneableInterface; | ||
20 | use Symfony\Component\Cache\ResettableInterface; | ||
21 | use Symfony\Component\Cache\Traits\ContractsTrait; | ||
22 | use 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 | */ | ||
36 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\InvalidArgumentException; | ||
15 | |||
16 | /** | ||
17 | * Interface for invalidating cached items using tags. | ||
18 | * | ||
19 | * @author Nicolas Grekas <p@tchwork.com> | ||
20 | */ | ||
21 | interface TagAwareAdapterInterface extends AdapterInterface | ||
22 | { | ||
23 | /** | ||
24 | * Invalidates cached items using tags. | ||
25 | * | ||
26 | * @param string[] $tags An array of tags to invalidate | ||
27 | * | ||
28 | * @throws InvalidArgumentException When $tags is not valid | ||
29 | */ | ||
30 | public function invalidateTags(array $tags): bool; | ||
31 | } | ||
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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Psr\Cache\CacheItemInterface; | ||
15 | use Symfony\Component\Cache\CacheItem; | ||
16 | use Symfony\Component\Cache\PruneableInterface; | ||
17 | use Symfony\Component\Cache\ResettableInterface; | ||
18 | use Symfony\Contracts\Cache\CacheInterface; | ||
19 | use 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 | */ | ||
28 | class 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 | */ | ||
242 | class 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 | |||
12 | namespace Symfony\Component\Cache\Adapter; | ||
13 | |||
14 | use Symfony\Contracts\Cache\TagAwareCacheInterface; | ||
15 | |||
16 | /** | ||
17 | * @author Robin Chalas <robin.chalas@gmail.com> | ||
18 | */ | ||
19 | class 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 | } | ||