vendor/logist/redis-bundle/src/LogistRedisBundle/Service/RedisClient.php line 62

Open in your IDE?
  1. <?php
  2. /*
  3.  *  This file is part of the logist\redis-bundle.
  4.  *
  5.  *  Copyright 2022 logist.cloud <support@logist.cloud>
  6.  */
  7. declare(strict_types 1);
  8. namespace LogistRedisBundle\Service;
  9. use Predis\Client;
  10. use Predis\PredisException;
  11. use Predis\Response\ServerException;
  12. use LogistRedisBundle\Bridge\ThresholdCheckerInterface;
  13. use LogistRedisBundle\Exception\LogistRedisUnserializationException;
  14. use LogistRedisBundle\Exception\LogistRedisInvalidCounterException;
  15. /**
  16.  * @author Grigoriy Kulin <yarboroda@gmail.com>
  17.  */
  18. class RedisClient implements ThresholdCheckerInterface
  19. {
  20.     public const TTL_UNLIMITED = -1;
  21.     public const TTL_KEY_NOT_EXISTS = -2;
  22.     private const INVALID_COUNTER_MSG 'ERR value is not an integer or out of range';
  23.     /**
  24.      * @var Client
  25.      */
  26.     protected $client;
  27.     /**
  28.      * @var string
  29.      */
  30.     private $serializedFalse;
  31.     /**
  32.      * @var string
  33.      */
  34.     private $serializedNull;
  35.     /**
  36.      * @var bool
  37.      */
  38.     private $binarySerialization;
  39.     /**
  40.      * @var string
  41.      */
  42.     private $prefix;
  43.     /**
  44.      * @param string $host
  45.      * @param integer $port
  46.      * @param string $username
  47.      * @param string $password
  48.      * @param string $prefix
  49.      */
  50.     public function __construct(string $hostint $portstring $usernamestring $passwordstring $prefixbool $binarySerialization)
  51.     {
  52.         $prefix "{$prefix}:";
  53.         $this->prefix $prefix;
  54.         $this->binarySerialization $binarySerialization;
  55.         $this->client = new Client([
  56.             'host' => $host,
  57.             'port' => $port,
  58.             'username' => $username,
  59.             'password' => $password,
  60.         ], [
  61.             'prefix' => $prefix,
  62.         ]);
  63.         $this->serializedFalse $this->serialize(false);
  64.         $this->serializedNull $this->serialize(null);
  65.     }
  66.     /**
  67.      * @return string
  68.      */
  69.     public function getPrefix()
  70.     {
  71.         return $this->prefix;
  72.     }
  73.     /**
  74.      * @param string $key
  75.      * @throws PredisException
  76.      * @throws LogistRedisUnserializationException
  77.      * @return mixed
  78.      */
  79.     public function get(string $key)
  80.     {
  81.         return $this->unserialize($this->client->get($key));
  82.     }
  83.     
  84.     /**
  85.      * @param string $prefix
  86.      * @throws PredisException
  87.      * @throws LogistRedisUnserializationException
  88.      * @return array
  89.      */
  90.     public function keys(string $prefix '*')
  91.     {
  92.         return $this->client->keys($prefix);
  93.     }
  94.     
  95.     /**
  96.      * @param string $key
  97.      * @throws PredisException
  98.      * @throws LogistRedisUnserializationException
  99.      * @return mixed
  100.      */
  101.     public function membersGet(string $key)
  102.     {
  103.         $result = [];
  104.         $members $this->client->smembers($key);
  105.         if(empty($members)){
  106.             return [];
  107.         }
  108.         foreach($members as $member){
  109.             $result[] = $this->unserialize($member);
  110.         }
  111.         return $result;
  112.     }
  113.     
  114.     /**
  115.      * check if key exists
  116.      * @param string $key
  117.      * @throws PredisException
  118.      * @return bool
  119.      */
  120.     public function exists(string $key)
  121.     {
  122.         $value $this->client->exists($key);
  123.         return ($value === 1);
  124.     }
  125.     /**
  126.      * @param string[] $keys
  127.      * @throws PredisException
  128.      * @throws LogistRedisUnserializationException
  129.      * @return array
  130.      */
  131.     public function getMultiple(array $keys)
  132.     {
  133.         if (empty($keys)) {
  134.             return [];
  135.         }
  136.         $values $this->client->mget($keys);
  137.         $result = [];
  138.         foreach ($values as $index => $value) {
  139.             $result[$keys[$index]] = $this->unserialize($value);
  140.         }
  141.         return $result;
  142.     }
  143.     /**
  144.      * get remaining time to live of key
  145.      * -2 if key does not exists
  146.      * -1 if key exists but has no ttl
  147.      * @param string $key
  148.      * @throws PredisException
  149.      * @return int seconds
  150.      */
  151.     public function ttl(string $key)
  152.     {
  153.         return $this->client->ttl($key);
  154.     }
  155.     /**
  156.      * @param string $key
  157.      * @param mixed $value
  158.      * @param integer|null $ttl time to live seconds
  159.      * @throws PredisException
  160.      * @return void
  161.      */
  162.     public function set(string $key$value, ?int $ttl null)
  163.     {
  164.         if (is_null($ttl) || $ttl 1) {
  165.             $this->client->set($key$this->serialize($value));
  166.         } else {
  167.             $this->client->setex($key$ttl$this->serialize($value));
  168.         }
  169.     }
  170.     
  171.     /**
  172.      * @param string $key
  173.      * @param array $values
  174.      * @param integer|null $ttl time to live seconds
  175.      * @throws PredisException
  176.      * @return void
  177.      */
  178.     public function membersAdd(string $key, array $values, ?int $ttl null)
  179.     {
  180.         foreach($values as $value){
  181.             $this->client->sadd($key$this->serialize($value));
  182.         }
  183.         if (!is_null($ttl) && $ttl 0) {
  184.             $this->expire($key$ttl);
  185.         }
  186.     }
  187.     /**
  188.      * cache result of callable $fn
  189.      * @param string $key
  190.      * @param callable $fn
  191.      * @param integer|null $ttl time to live seconds
  192.      * @throws PredisException
  193.      * @throws LogistRedisUnserializationException
  194.      * @return mixed
  195.      */
  196.     public function cache(string $key, callable $fn, ?int $ttl null)
  197.     {
  198.         if ($this->exists($key)) {
  199.             return $this->get($key);
  200.         }
  201.         $value $fn();
  202.         $this->set($key$value$ttl);
  203.         return $value;
  204.     }
  205.     /**
  206.      * set or update time to live for key
  207.      * @param string $key
  208.      * @param integer $ttl seconds
  209.      * @throws PredisException
  210.      * @return void
  211.      */
  212.     public function expire(string $keyint $ttl)
  213.     {
  214.         $this->client->expire($key$ttl);
  215.     }
  216.     /**
  217.      * set or update time to live for key till date
  218.      * @param string $key
  219.      * @param \DateTime $expireDate
  220.      * @throws PredisException
  221.      * @return void
  222.      */
  223.     public function expireAt(string $key\DateTime $expireDate)
  224.     {
  225.         $this->client->expireat($key$expireDate->getTimestamp());
  226.     }
  227.     /**
  228.      * @param string $key
  229.      * @throws PredisException
  230.      * @return void
  231.      */
  232.     public function delete(string $key)
  233.     {
  234.         $this->client->del($key);
  235.     }
  236.     
  237.     /**
  238.      * @param string $key
  239.      * @param array $values
  240.      * @throws PredisException
  241.      * @return void
  242.      */
  243.     public function membersRemove(string $key, array $values)
  244.     {
  245.         $this->client->srem($key$values);
  246.     }
  247.     /**
  248.      * @param string[] $keys
  249.      * @throws PredisException
  250.      * @return void
  251.      */
  252.     public function deleteMultiple(array $keys)
  253.     {
  254.         $this->client->del($keys);
  255.     }
  256.     /**
  257.      * get counter value and verify it is valid
  258.      * @param string $key
  259.      * @throws PredisException
  260.      * @throws LogistRedisInvalidCounterException
  261.      * @return int
  262.      */
  263.     public function getCounter(string $key)
  264.     {
  265.         $value $this->client->get($key);
  266.         if (empty($value) || !ctype_digit($value)) {
  267.             throw new LogistRedisInvalidCounterException("Not valid counter value in key {$key}");
  268.         }
  269.         return (int) $value;
  270.     }
  271.     /**
  272.      * increment value of integer key by $value
  273.      * @param string $key
  274.      * @param int $value
  275.      * @throws PredisException
  276.      * @throws LogistRedisInvalidCounterException
  277.      * @return int new value
  278.      */
  279.     public function increment(string $keyint $value 1)
  280.     {
  281.         try {
  282.             if ($value === 1) {
  283.                 $result $this->client->incr($key);
  284.             } else {
  285.                 $result $this->client->incrby($key$value);
  286.             }
  287.         } catch (ServerException $e) {
  288.             if ($e->getMessage() == self::INVALID_COUNTER_MSG) {
  289.                 throw new LogistRedisInvalidCounterException("Not valid counter value in key {$key}");
  290.             }
  291.     
  292.             throw $e;
  293.         }
  294.         $result = (int) $result;
  295.         $this->checkCounterValue($result$key);
  296.         return $result;
  297.     }
  298.     /**
  299.      * decrement value of integer key by $value
  300.      * @param string $key
  301.      * @param int $value
  302.      * @throws PredisException
  303.      * @throws LogistRedisInvalidCounterException
  304.      * @return int new value
  305.      */
  306.     public function decrement(string $keyint $value 1)
  307.     {
  308.         try {
  309.             if ($value === 1) {
  310.                 $result $this->client->decr($key);
  311.             } else {
  312.                 $result $this->client->decrby($key$value);
  313.             }
  314.         } catch (ServerException $e) {
  315.             if ($e->getMessage() == self::INVALID_COUNTER_MSG) {
  316.                 throw new LogistRedisInvalidCounterException("Not valid counter value in key {$key}");
  317.             }
  318.     
  319.             throw $e;
  320.         }
  321.         $result = (int) $result;
  322.         $this->checkCounterValue($result$key);
  323.         return $result;
  324.     }
  325.     /**
  326.      * @param integer $result
  327.      * @param string $key
  328.      * @throws PredisException
  329.      * @throws LogistRedisInvalidCounterException
  330.      * @return void
  331.      */
  332.     private function checkCounterValue(int $resultstring $key)
  333.     {
  334.         if ($result 0) {
  335.             $ttl $this->ttl($key);
  336.             $ttl = ($ttl 0) ? null $ttl;
  337.             $this->set($key$result$ttl);
  338.             throw new LogistRedisInvalidCounterException('Key value got lower then 0. It was serialized and it can not be used in counter.');
  339.         }
  340.     }
  341.     /**
  342.      * @param mixed $value
  343.      * @return string|int
  344.      */
  345.     private function serialize($value)
  346.     {
  347.         if (is_int($value) && ($value >= 0)) {
  348.             return $value;
  349.         }
  350.         if ($this->binarySerialization) {
  351.             return igbinary_serialize($value);
  352.         }
  353.         return serialize($value);
  354.     }
  355.     /**
  356.      * @param string|null $value
  357.      * @throws LogistRedisUnserializationException
  358.      * @return mixed
  359.      */
  360.     private function unserialize(?string $value)
  361.     {
  362.         if (is_null($value)) {
  363.             return null;
  364.         }
  365.         if ($value === '') {
  366.             return '';
  367.         }
  368.         if (ctype_digit($value)) {
  369.             return (int) $value;
  370.         }
  371.         if ($value === $this->serializedFalse) {
  372.             return false;
  373.         }
  374.         if ($value === $this->serializedNull) {
  375.             return null;
  376.         }
  377.         $result $this->binarySerialization igbinary_unserialize($value) : unserialize($value);
  378.         if (($result === false) || is_null($result)) {
  379.             throw new LogistRedisUnserializationException('Stored value cannot be unserialized');
  380.         }
  381.         return $result;
  382.     }
  383.     /**
  384.      * delete all data in prefix with optional mask
  385.      * @var string mask begin of keys to reset
  386.      * @return int deleted keys counter
  387.      */
  388.     public function resetPrefix($mask='')
  389.     {
  390.         $options = ['match' => "{$this->prefix}{$mask}*"];
  391.         $i 0;
  392.         $cnt 0;
  393.         do {
  394.             $response $this->client->scan($i$options);
  395.             $keys array_map(function($key) {
  396.                 return preg_replace("/^{$this->prefix}/"''$key);
  397.             }, $response[1]);
  398.             if (!empty($keys)) {
  399.                 $cnt += count($keys);
  400.                 $this->client->del($keys);
  401.             }
  402.             $i = (int) $response[0];
  403.         } while ($i 0);
  404.         return $cnt;
  405.     }
  406.     /**
  407.      * @param string $key
  408.      * @param int $limit
  409.      * @param int $timeInterval in seconds
  410.      * @param int $increment value to increment counter
  411.      * @return bool
  412.      */
  413.     public function checkThreshold(string $keyint $limitint $timeIntervalint $increment 1)
  414.     {
  415.         $count $this->increment($key$increment);
  416.         if ($count $limit) {
  417.             return false;
  418.         }
  419.         if ($count === $increment) {
  420.             $this->expire($key$timeInterval);
  421.         }
  422.         return true;
  423.     }
  424. }