vendor/symfony/cache/Adapter/MemcachedAdapter.php line 266

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Cache\Adapter;
  11. use Symfony\Component\Cache\Exception\CacheException;
  12. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  13. use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
  14. use Symfony\Component\Cache\Marshaller\MarshallerInterface;
  15. /**
  16.  * @author Rob Frawley 2nd <rmf@src.run>
  17.  * @author Nicolas Grekas <p@tchwork.com>
  18.  */
  19. class MemcachedAdapter extends AbstractAdapter
  20. {
  21.     /**
  22.      * We are replacing characters that are illegal in Memcached keys with reserved characters from
  23.      * {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached.
  24.      * Note: don’t use {@see \Symfony\Component\Cache\Adapter\AbstractAdapter::NS_SEPARATOR}.
  25.      */
  26.     private const RESERVED_MEMCACHED " \n\r\t\v\f\0";
  27.     private const RESERVED_PSR6 '@()\{}/';
  28.     protected $maxIdLength 250;
  29.     private const DEFAULT_CLIENT_OPTIONS = [
  30.         'persistent_id' => null,
  31.         'username' => null,
  32.         'password' => null,
  33.         \Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP,
  34.     ];
  35.     private $marshaller;
  36.     private $client;
  37.     private $lazyClient;
  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 $clientstring $namespace ''int $defaultLifetime 0MarshallerInterface $marshaller null)
  49.     {
  50.         if (!static::isSupported()) {
  51.             throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 '> 3.1.5' '>= 2.2.0').' is required.');
  52.         }
  53.         if ('Memcached' === \get_class($client)) {
  54.             $opt $client->getOption(\Memcached::OPT_SERIALIZER);
  55.             if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
  56.                 throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
  57.             }
  58.             $this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
  59.             $this->client $client;
  60.         } else {
  61.             $this->lazyClient $client;
  62.         }
  63.         parent::__construct($namespace$defaultLifetime);
  64.         $this->enableVersioning();
  65.         $this->marshaller $marshaller ?? new DefaultMarshaller();
  66.     }
  67.     public static function isSupported()
  68.     {
  69.         return \extension_loaded('memcached') && version_compare(phpversion('memcached'), \PHP_VERSION_ID >= 80100 '3.1.6' '2.2.0''>=');
  70.     }
  71.     /**
  72.      * Creates a Memcached instance.
  73.      *
  74.      * By default, the binary protocol, no block, and libketama compatible options are enabled.
  75.      *
  76.      * Examples for servers:
  77.      * - 'memcached://user:pass@localhost?weight=33'
  78.      * - [['localhost', 11211, 33]]
  79.      *
  80.      * @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs
  81.      *
  82.      * @return \Memcached
  83.      *
  84.      * @throws \ErrorException When invalid options or servers are provided
  85.      */
  86.     public static function createConnection($servers, array $options = [])
  87.     {
  88.         if (\is_string($servers)) {
  89.             $servers = [$servers];
  90.         } elseif (!\is_array($servers)) {
  91.             throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.'get_debug_type($servers)));
  92.         }
  93.         if (!static::isSupported()) {
  94.             throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 '> 3.1.5' '>= 2.2.0').' is required.');
  95.         }
  96.         set_error_handler(function ($type$msg$file$line) { throw new \ErrorException($msg0$type$file$line); });
  97.         try {
  98.             $options += static::DEFAULT_CLIENT_OPTIONS;
  99.             $client = new \Memcached($options['persistent_id']);
  100.             $username $options['username'];
  101.             $password $options['password'];
  102.             // parse any DSN in $servers
  103.             foreach ($servers as $i => $dsn) {
  104.                 if (\is_array($dsn)) {
  105.                     continue;
  106.                 }
  107.                 if (!str_starts_with($dsn'memcached:')) {
  108.                     throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s" does not start with "memcached:".'$dsn));
  109.                 }
  110.                 $params preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
  111.                     if (!empty($m[2])) {
  112.                         [$username$password] = explode(':'$m[2], 2) + [=> null];
  113.                     }
  114.                     return 'file:'.($m[1] ?? '');
  115.                 }, $dsn);
  116.                 if (false === $params parse_url($params)) {
  117.                     throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".'$dsn));
  118.                 }
  119.                 $query $hosts = [];
  120.                 if (isset($params['query'])) {
  121.                     parse_str($params['query'], $query);
  122.                     if (isset($query['host'])) {
  123.                         if (!\is_array($hosts $query['host'])) {
  124.                             throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".'$dsn));
  125.                         }
  126.                         foreach ($hosts as $host => $weight) {
  127.                             if (false === $port strrpos($host':')) {
  128.                                 $hosts[$host] = [$host11211, (int) $weight];
  129.                             } else {
  130.                                 $hosts[$host] = [substr($host0$port), (int) substr($host$port), (int) $weight];
  131.                             }
  132.                         }
  133.                         $hosts array_values($hosts);
  134.                         unset($query['host']);
  135.                     }
  136.                     if ($hosts && !isset($params['host']) && !isset($params['path'])) {
  137.                         unset($servers[$i]);
  138.                         $servers array_merge($servers$hosts);
  139.                         continue;
  140.                     }
  141.                 }
  142.                 if (!isset($params['host']) && !isset($params['path'])) {
  143.                     throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".'$dsn));
  144.                 }
  145.                 if (isset($params['path']) && preg_match('#/(\d+)$#'$params['path'], $m)) {
  146.                     $params['weight'] = $m[1];
  147.                     $params['path'] = substr($params['path'], 0, -\strlen($m[0]));
  148.                 }
  149.                 $params += [
  150.                     'host' => $params['host'] ?? $params['path'],
  151.                     'port' => isset($params['host']) ? 11211 null,
  152.                     'weight' => 0,
  153.                 ];
  154.                 if ($query) {
  155.                     $params += $query;
  156.                     $options $query $options;
  157.                 }
  158.                 $servers[$i] = [$params['host'], $params['port'], $params['weight']];
  159.                 if ($hosts) {
  160.                     $servers array_merge($servers$hosts);
  161.                 }
  162.             }
  163.             // set client's options
  164.             unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
  165.             $options array_change_key_case($options\CASE_UPPER);
  166.             $client->setOption(\Memcached::OPT_BINARY_PROTOCOLtrue);
  167.             $client->setOption(\Memcached::OPT_NO_BLOCKtrue);
  168.             $client->setOption(\Memcached::OPT_TCP_NODELAYtrue);
  169.             if (!\array_key_exists('LIBKETAMA_COMPATIBLE'$options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE$options)) {
  170.                 $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLEtrue);
  171.             }
  172.             foreach ($options as $name => $value) {
  173.                 if (\is_int($name)) {
  174.                     continue;
  175.                 }
  176.                 if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
  177.                     $value \constant('Memcached::'.$name.'_'.strtoupper($value));
  178.                 }
  179.                 unset($options[$name]);
  180.                 if (\defined('Memcached::OPT_'.$name)) {
  181.                     $options[\constant('Memcached::OPT_'.$name)] = $value;
  182.                 }
  183.             }
  184.             $client->setOptions($options);
  185.             // set client's servers, taking care of persistent connections
  186.             if (!$client->isPristine()) {
  187.                 $oldServers = [];
  188.                 foreach ($client->getServerList() as $server) {
  189.                     $oldServers[] = [$server['host'], $server['port']];
  190.                 }
  191.                 $newServers = [];
  192.                 foreach ($servers as $server) {
  193.                     if (\count($server)) {
  194.                         $server array_values($server);
  195.                         unset($server[2]);
  196.                         $server[1] = (int) $server[1];
  197.                     }
  198.                     $newServers[] = $server;
  199.                 }
  200.                 if ($oldServers !== $newServers) {
  201.                     $client->resetServerList();
  202.                     $client->addServers($servers);
  203.                 }
  204.             } else {
  205.                 $client->addServers($servers);
  206.             }
  207.             if (null !== $username || null !== $password) {
  208.                 if (!method_exists($client'setSaslAuthData')) {
  209.                     trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
  210.                 }
  211.                 $client->setSaslAuthData($username$password);
  212.             }
  213.             return $client;
  214.         } finally {
  215.             restore_error_handler();
  216.         }
  217.     }
  218.     /**
  219.      * {@inheritdoc}
  220.      */
  221.     protected function doSave(array $valuesint $lifetime)
  222.     {
  223.         if (!$values $this->marshaller->marshall($values$failed)) {
  224.             return $failed;
  225.         }
  226.         if ($lifetime && $lifetime 30 86400) {
  227.             $lifetime += time();
  228.         }
  229.         $encodedValues = [];
  230.         foreach ($values as $key => $value) {
  231.             $encodedValues[self::encodeKey($key)] = $value;
  232.         }
  233.         return $this->checkResultCode($this->getClient()->setMulti($encodedValues$lifetime)) ? $failed false;
  234.     }
  235.     /**
  236.      * {@inheritdoc}
  237.      */
  238.     protected function doFetch(array $ids)
  239.     {
  240.         try {
  241.             $encodedIds array_map([__CLASS__'encodeKey'], $ids);
  242.             $encodedResult $this->checkResultCode($this->getClient()->getMulti($encodedIds));
  243.             $result = [];
  244.             foreach ($encodedResult as $key => $value) {
  245.                 $result[self::decodeKey($key)] = $this->marshaller->unmarshall($value);
  246.             }
  247.             return $result;
  248.         } catch (\Error $e) {
  249.             throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR$e->getFile(), $e->getLine());
  250.         }
  251.     }
  252.     /**
  253.      * {@inheritdoc}
  254.      */
  255.     protected function doHave(string $id)
  256.     {
  257.         return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
  258.     }
  259.     /**
  260.      * {@inheritdoc}
  261.      */
  262.     protected function doDelete(array $ids)
  263.     {
  264.         $ok true;
  265.         $encodedIds array_map([__CLASS__'encodeKey'], $ids);
  266.         foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) {
  267.             if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
  268.                 $ok false;
  269.             }
  270.         }
  271.         return $ok;
  272.     }
  273.     /**
  274.      * {@inheritdoc}
  275.      */
  276.     protected function doClear(string $namespace)
  277.     {
  278.         return '' === $namespace && $this->getClient()->flush();
  279.     }
  280.     private function checkResultCode($result)
  281.     {
  282.         $code $this->client->getResultCode();
  283.         if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
  284.             return $result;
  285.         }
  286.         throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage()));
  287.     }
  288.     private function getClient(): \Memcached
  289.     {
  290.         if ($this->client) {
  291.             return $this->client;
  292.         }
  293.         $opt $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER);
  294.         if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
  295.             throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
  296.         }
  297.         if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) {
  298.             throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.'$prefix));
  299.         }
  300.         return $this->client $this->lazyClient;
  301.     }
  302.     private static function encodeKey(string $key): string
  303.     {
  304.         return strtr($keyself::RESERVED_MEMCACHEDself::RESERVED_PSR6);
  305.     }
  306.     private static function decodeKey(string $key): string
  307.     {
  308.         return strtr($keyself::RESERVED_PSR6self::RESERVED_MEMCACHED);
  309.     }
  310. }