diff options
Diffstat (limited to 'vendor/symfony/cache/Adapter/MemcachedAdapter.php')
-rw-r--r-- | vendor/symfony/cache/Adapter/MemcachedAdapter.php | 329 |
1 files changed, 329 insertions, 0 deletions
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 | } | ||