2018-04-17 02:11:51 +00:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Cache\Traits ;
use Symfony\Component\Cache\Exception\CacheException ;
use Symfony\Component\Cache\Exception\InvalidArgumentException ;
2024-03-20 02:35:09 +00:00
use Symfony\Component\Cache\Marshaller\DefaultMarshaller ;
use Symfony\Component\Cache\Marshaller\MarshallerInterface ;
2018-04-17 02:11:51 +00:00
/**
* @ author Rob Frawley 2 nd < rmf @ src . run >
* @ author Nicolas Grekas < p @ tchwork . com >
*
* @ internal
*/
trait MemcachedTrait
{
2024-01-12 05:08:24 +00:00
private static $defaultClientOptions = [
2018-04-17 02:11:51 +00:00
'persistent_id' => null ,
'username' => null ,
'password' => null ,
2024-01-12 05:08:24 +00:00
\Memcached :: OPT_SERIALIZER => \Memcached :: SERIALIZER_PHP ,
];
2018-04-17 02:11:51 +00:00
2024-03-20 02:35:09 +00:00
/**
* We are replacing characters that are illegal in Memcached keys with reserved characters from
* { @ see \Symfony\Contracts\Cache\ItemInterface :: RESERVED_CHARACTERS } that are legal in Memcached .
* Note : don’ t use { @ see \Symfony\Component\Cache\Adapter\AbstractAdapter :: NS_SEPARATOR } .
*/
private static $RESERVED_MEMCACHED = " \n \r \t \ v \ f \0 " ;
private static $RESERVED_PSR6 = '@()\{}/' ;
private $marshaller ;
2018-04-17 02:11:51 +00:00
private $client ;
private $lazyClient ;
public static function isSupported ()
{
2024-03-20 02:35:09 +00:00
return \extension_loaded ( 'memcached' ) && version_compare ( phpversion ( 'memcached' ), \PHP_VERSION_ID >= 80100 ? '3.1.6' : '2.2.0' , '>=' );
2018-04-17 02:11:51 +00:00
}
2024-03-20 02:35:09 +00:00
private function init ( \Memcached $client , string $namespace , int $defaultLifetime , ? MarshallerInterface $marshaller )
2018-04-17 02:11:51 +00:00
{
if ( ! static :: isSupported ()) {
2024-03-20 02:35:09 +00:00
throw new CacheException ( 'Memcached ' . ( \PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0' ) . ' is required.' );
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
if ( 'Memcached' === \get_class ( $client )) {
2018-04-17 02:11:51 +00:00
$opt = $client -> getOption ( \Memcached :: OPT_SERIALIZER );
if ( \Memcached :: SERIALIZER_PHP !== $opt && \Memcached :: SERIALIZER_IGBINARY !== $opt ) {
throw new CacheException ( 'MemcachedAdapter: "serializer" option must be "php" or "igbinary".' );
}
2024-01-12 05:08:24 +00:00
$this -> maxIdLength -= \strlen ( $client -> getOption ( \Memcached :: OPT_PREFIX_KEY ));
2018-04-17 02:11:51 +00:00
$this -> client = $client ;
} else {
$this -> lazyClient = $client ;
}
parent :: __construct ( $namespace , $defaultLifetime );
$this -> enableVersioning ();
2024-03-20 02:35:09 +00:00
$this -> marshaller = $marshaller ? ? new DefaultMarshaller ();
2018-04-17 02:11:51 +00:00
}
/**
* Creates a Memcached instance .
*
* By default , the binary protocol , no block , and libketama compatible options are enabled .
*
* Examples for servers :
* - 'memcached://user:pass@localhost?weight=33'
2024-01-12 05:08:24 +00:00
* - [[ 'localhost' , 11211 , 33 ]]
2018-04-17 02:11:51 +00:00
*
2024-01-12 05:08:24 +00:00
* @ param array [] | string | string [] $servers An array of servers , a DSN , or an array of DSNs
2018-04-17 02:11:51 +00:00
*
* @ return \Memcached
*
* @ throws \ErrorException When invalid options or servers are provided
*/
2024-01-12 05:08:24 +00:00
public static function createConnection ( $servers , array $options = [])
2018-04-17 02:11:51 +00:00
{
2024-01-12 05:08:24 +00:00
if ( \is_string ( $servers )) {
$servers = [ $servers ];
} elseif ( ! \is_array ( $servers )) {
throw new InvalidArgumentException ( sprintf ( 'MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.' , \gettype ( $servers )));
2018-04-17 02:11:51 +00:00
}
if ( ! static :: isSupported ()) {
2024-03-20 02:35:09 +00:00
throw new CacheException ( 'Memcached ' . ( \PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0' ) . ' is required.' );
2018-04-17 02:11:51 +00:00
}
set_error_handler ( function ( $type , $msg , $file , $line ) { throw new \ErrorException ( $msg , 0 , $type , $file , $line ); });
try {
$options += static :: $defaultClientOptions ;
$client = new \Memcached ( $options [ 'persistent_id' ]);
$username = $options [ 'username' ];
$password = $options [ 'password' ];
// parse any DSN in $servers
foreach ( $servers as $i => $dsn ) {
2024-01-12 05:08:24 +00:00
if ( \is_array ( $dsn )) {
2018-04-17 02:11:51 +00:00
continue ;
}
2024-03-20 02:35:09 +00:00
if ( ! str_starts_with ( $dsn , 'memcached:' )) {
throw new InvalidArgumentException ( sprintf ( 'Invalid Memcached DSN: "%s" does not start with "memcached:".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
2024-03-20 02:35:09 +00:00
$params = preg_replace_callback ( '#^memcached:(//)?(?:([^@]*+)@)?#' , function ( $m ) use ( & $username , & $password ) {
if ( ! empty ( $m [ 2 ])) {
[ $username , $password ] = explode ( ':' , $m [ 2 ], 2 ) + [ 1 => null ];
2018-04-17 02:11:51 +00:00
}
2024-03-20 02:35:09 +00:00
return 'file:' . ( $m [ 1 ] ? ? '' );
2018-04-17 02:11:51 +00:00
}, $dsn );
if ( false === $params = parse_url ( $params )) {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( 'Invalid Memcached DSN: "%s".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
2024-03-20 02:35:09 +00:00
$query = $hosts = [];
if ( isset ( $params [ 'query' ])) {
parse_str ( $params [ 'query' ], $query );
if ( isset ( $query [ 'host' ])) {
if ( ! \is_array ( $hosts = $query [ 'host' ])) {
throw new InvalidArgumentException ( sprintf ( 'Invalid Memcached DSN: "%s".' , $dsn ));
}
foreach ( $hosts as $host => $weight ) {
if ( false === $port = strrpos ( $host , ':' )) {
$hosts [ $host ] = [ $host , 11211 , ( int ) $weight ];
} else {
$hosts [ $host ] = [ substr ( $host , 0 , $port ), ( int ) substr ( $host , 1 + $port ), ( int ) $weight ];
}
}
$hosts = array_values ( $hosts );
unset ( $query [ 'host' ]);
}
if ( $hosts && ! isset ( $params [ 'host' ]) && ! isset ( $params [ 'path' ])) {
unset ( $servers [ $i ]);
$servers = array_merge ( $servers , $hosts );
continue ;
}
}
2018-04-17 02:11:51 +00:00
if ( ! isset ( $params [ 'host' ]) && ! isset ( $params [ 'path' ])) {
2024-01-12 05:08:24 +00:00
throw new InvalidArgumentException ( sprintf ( 'Invalid Memcached DSN: "%s".' , $dsn ));
2018-04-17 02:11:51 +00:00
}
if ( isset ( $params [ 'path' ]) && preg_match ( '#/(\d+)$#' , $params [ 'path' ], $m )) {
$params [ 'weight' ] = $m [ 1 ];
2024-01-12 05:08:24 +00:00
$params [ 'path' ] = substr ( $params [ 'path' ], 0 , - \strlen ( $m [ 0 ]));
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
$params += [
2024-03-20 02:35:09 +00:00
'host' => $params [ 'host' ] ? ? $params [ 'path' ],
2018-04-17 02:11:51 +00:00
'port' => isset ( $params [ 'host' ]) ? 11211 : null ,
'weight' => 0 ,
2024-01-12 05:08:24 +00:00
];
2024-03-20 02:35:09 +00:00
if ( $query ) {
2018-04-17 02:11:51 +00:00
$params += $query ;
$options = $query + $options ;
}
2024-01-12 05:08:24 +00:00
$servers [ $i ] = [ $params [ 'host' ], $params [ 'port' ], $params [ 'weight' ]];
2024-03-20 02:35:09 +00:00
if ( $hosts ) {
$servers = array_merge ( $servers , $hosts );
}
2018-04-17 02:11:51 +00:00
}
// set client's options
unset ( $options [ 'persistent_id' ], $options [ 'username' ], $options [ 'password' ], $options [ 'weight' ], $options [ 'lazy' ]);
2024-01-12 05:08:24 +00:00
$options = array_change_key_case ( $options , \CASE_UPPER );
2018-04-17 02:11:51 +00:00
$client -> setOption ( \Memcached :: OPT_BINARY_PROTOCOL , true );
$client -> setOption ( \Memcached :: OPT_NO_BLOCK , true );
2024-01-12 05:08:24 +00:00
$client -> setOption ( \Memcached :: OPT_TCP_NODELAY , true );
if ( ! \array_key_exists ( 'LIBKETAMA_COMPATIBLE' , $options ) && ! \array_key_exists ( \Memcached :: OPT_LIBKETAMA_COMPATIBLE , $options )) {
2018-04-17 02:11:51 +00:00
$client -> setOption ( \Memcached :: OPT_LIBKETAMA_COMPATIBLE , true );
}
foreach ( $options as $name => $value ) {
2024-01-12 05:08:24 +00:00
if ( \is_int ( $name )) {
2018-04-17 02:11:51 +00:00
continue ;
}
if ( 'HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name ) {
2024-01-12 05:08:24 +00:00
$value = \constant ( 'Memcached::' . $name . '_' . strtoupper ( $value ));
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
$opt = \constant ( 'Memcached::OPT_' . $name );
2018-04-17 02:11:51 +00:00
unset ( $options [ $name ]);
$options [ $opt ] = $value ;
}
$client -> setOptions ( $options );
// set client's servers, taking care of persistent connections
if ( ! $client -> isPristine ()) {
2024-01-12 05:08:24 +00:00
$oldServers = [];
2018-04-17 02:11:51 +00:00
foreach ( $client -> getServerList () as $server ) {
2024-01-12 05:08:24 +00:00
$oldServers [] = [ $server [ 'host' ], $server [ 'port' ]];
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
$newServers = [];
2018-04-17 02:11:51 +00:00
foreach ( $servers as $server ) {
2024-01-12 05:08:24 +00:00
if ( 1 < \count ( $server )) {
2018-04-17 02:11:51 +00:00
$server = array_values ( $server );
unset ( $server [ 2 ]);
$server [ 1 ] = ( int ) $server [ 1 ];
}
$newServers [] = $server ;
}
if ( $oldServers !== $newServers ) {
$client -> resetServerList ();
2024-01-12 05:08:24 +00:00
$client -> addServers ( $servers );
2018-04-17 02:11:51 +00:00
}
2024-01-12 05:08:24 +00:00
} else {
$client -> addServers ( $servers );
2018-04-17 02:11:51 +00:00
}
if ( null !== $username || null !== $password ) {
if ( ! method_exists ( $client , 'setSaslAuthData' )) {
trigger_error ( 'Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.' );
}
$client -> setSaslAuthData ( $username , $password );
}
return $client ;
} finally {
restore_error_handler ();
}
}
/**
* { @ inheritdoc }
*/
2024-03-20 02:35:09 +00:00
protected function doSave ( array $values , int $lifetime )
2018-04-17 02:11:51 +00:00
{
2024-03-20 02:35:09 +00:00
if ( ! $values = $this -> marshaller -> marshall ( $values , $failed )) {
return $failed ;
}
2018-04-17 02:11:51 +00:00
if ( $lifetime && $lifetime > 30 * 86400 ) {
$lifetime += time ();
}
2024-01-12 05:08:24 +00:00
$encodedValues = [];
foreach ( $values as $key => $value ) {
2024-03-20 02:35:09 +00:00
$encodedValues [ self :: encodeKey ( $key )] = $value ;
2024-01-12 05:08:24 +00:00
}
2024-03-20 02:35:09 +00:00
return $this -> checkResultCode ( $this -> getClient () -> setMulti ( $encodedValues , $lifetime )) ? $failed : false ;
2018-04-17 02:11:51 +00:00
}
/**
* { @ inheritdoc }
*/
protected function doFetch ( array $ids )
{
try {
2024-03-20 02:35:09 +00:00
$encodedIds = array_map ([ __CLASS__ , 'encodeKey' ], $ids );
2024-01-12 05:08:24 +00:00
$encodedResult = $this -> checkResultCode ( $this -> getClient () -> getMulti ( $encodedIds ));
$result = [];
foreach ( $encodedResult as $key => $value ) {
2024-03-20 02:35:09 +00:00
$result [ self :: decodeKey ( $key )] = $this -> marshaller -> unmarshall ( $value );
2024-01-12 05:08:24 +00:00
}
return $result ;
2018-04-17 02:11:51 +00:00
} catch ( \Error $e ) {
2024-01-12 05:08:24 +00:00
throw new \ErrorException ( $e -> getMessage (), $e -> getCode (), \E_ERROR , $e -> getFile (), $e -> getLine ());
2018-04-17 02:11:51 +00:00
}
}
/**
* { @ inheritdoc }
*/
protected function doHave ( $id )
{
2024-03-20 02:35:09 +00:00
return false !== $this -> getClient () -> get ( self :: encodeKey ( $id )) || $this -> checkResultCode ( \Memcached :: RES_SUCCESS === $this -> client -> getResultCode ());
2018-04-17 02:11:51 +00:00
}
/**
* { @ inheritdoc }
*/
protected function doDelete ( array $ids )
{
$ok = true ;
2024-03-20 02:35:09 +00:00
$encodedIds = array_map ([ __CLASS__ , 'encodeKey' ], $ids );
2024-01-12 05:08:24 +00:00
foreach ( $this -> checkResultCode ( $this -> getClient () -> deleteMulti ( $encodedIds )) as $result ) {
2018-04-17 02:11:51 +00:00
if ( \Memcached :: RES_SUCCESS !== $result && \Memcached :: RES_NOTFOUND !== $result ) {
$ok = false ;
2024-01-12 05:08:24 +00:00
break ;
2018-04-17 02:11:51 +00:00
}
}
return $ok ;
}
/**
* { @ inheritdoc }
*/
protected function doClear ( $namespace )
{
2024-01-12 05:08:24 +00:00
return '' === $namespace && $this -> getClient () -> flush ();
2018-04-17 02:11:51 +00:00
}
private function checkResultCode ( $result )
{
$code = $this -> client -> getResultCode ();
if ( \Memcached :: RES_SUCCESS === $code || \Memcached :: RES_NOTFOUND === $code ) {
return $result ;
}
2024-01-12 05:08:24 +00:00
throw new CacheException ( 'MemcachedAdapter client error: ' . strtolower ( $this -> client -> getResultMessage ()));
2018-04-17 02:11:51 +00:00
}
2024-03-20 02:35:09 +00:00
private function getClient () : \Memcached
2018-04-17 02:11:51 +00:00
{
if ( $this -> client ) {
return $this -> client ;
}
$opt = $this -> lazyClient -> getOption ( \Memcached :: OPT_SERIALIZER );
if ( \Memcached :: SERIALIZER_PHP !== $opt && \Memcached :: SERIALIZER_IGBINARY !== $opt ) {
throw new CacheException ( 'MemcachedAdapter: "serializer" option must be "php" or "igbinary".' );
}
if ( '' !== $prefix = ( string ) $this -> lazyClient -> getOption ( \Memcached :: OPT_PREFIX_KEY )) {
throw new CacheException ( sprintf ( 'MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.' , $prefix ));
}
return $this -> client = $this -> lazyClient ;
}
2024-03-20 02:35:09 +00:00
private static function encodeKey ( string $key ) : string
{
return strtr ( $key , self :: $RESERVED_MEMCACHED , self :: $RESERVED_PSR6 );
}
private static function decodeKey ( string $key ) : string
{
return strtr ( $key , self :: $RESERVED_PSR6 , self :: $RESERVED_MEMCACHED );
}
2018-04-17 02:11:51 +00:00
}