Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.31% covered (success)
98.31%
58 / 59
93.33% covered (success)
93.33%
14 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Memcached
98.31% covered (success)
98.31%
58 / 59
93.33% covered (success)
93.33%
14 / 15
29
0.00% covered (danger)
0.00%
0 / 1
 __construct
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 memcached
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addServer
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addServers
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getItemTtl
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 saveItem
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getItem
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 hasItem
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteItem
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 clear
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 destroy
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 incrementItem
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 decrementItem
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 fetchVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Cache\Adapter;
16
17use Pop\Cache\Clock;
18
19/**
20 * Memcached cache adapter class
21 *
22 * @category   Pop
23 * @package    Pop\Cache
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.0.0
28 */
29class Memcached extends AbstractAdapter
30{
31
32    /**
33     * Traits
34     */
35    use NamespacedVersionedKeys;
36
37    /**
38     * Memcached object
39     * @var ?\Memcached
40     */
41    protected ?\Memcached $memcached = null;
42
43    /**
44     * Memcached version
45     * @var ?string
46     */
47    protected ?string $version = null;
48
49    /**
50     * Cache namespace
51     * @var string
52     */
53    protected string $namespace = 'pop_cache';
54
55    /**
56     * Constructor
57     *
58     * Instantiate the memcached cache object
59     *
60     * @param  int    $ttl
61     * @param  string $host
62     * @param  int    $port
63     * @param  int    $weight
64     * @param  string $namespace
65     * @param  Clock\ClockInterface $clock
66     * @throws Exception
67     */
68    public function __construct(
69        int $ttl = 0, string $host = 'localhost', int $port = 11211, int $weight = 1,
70        string $namespace = 'pop_cache', Clock\ClockInterface $clock = new Clock\SystemClock()
71    )
72    {
73        parent::__construct($ttl, $clock);
74        if (!class_exists('Memcached', false)) {
75            throw new Exception('Error: Memcached is not available.');
76        }
77
78        $this->namespace = $namespace;
79        $this->memcached = new \Memcached();
80        $this->memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
81        $this->addServer($host, $port, $weight);
82
83        $version = $this->memcached->getVersion();
84        if (isset($version[$host . ':' . $port])) {
85            $this->version = $version[$host . ':' . $port];
86        }
87    }
88
89    /**
90     * Get the memcached object.
91     *
92     * @return \Memcached
93     */
94    public function memcached(): \Memcached
95    {
96        return $this->memcached;
97    }
98
99    /**
100     * Get the current version of memcached.
101     *
102     * @param  string $host
103     * @param  int    $port
104     * @param  int    $weight
105     * @return Memcached
106     */
107    public function addServer(string $host, int $port = 11211, int $weight = 1): Memcached
108    {
109        $this->memcached->addServer($host, $port, $weight);
110        return $this;
111    }
112
113    /**
114     * Get the current version of memcached.
115     *
116     * @param  array $servers
117     * @return Memcached
118     */
119    public function addServers(array $servers): Memcached
120    {
121        $this->memcached->addServers($servers);
122        return $this;
123    }
124
125    /**
126     * Get the current version of memcached.
127     *
128     * @return ?string
129     */
130    public function getVersion(): ?string
131    {
132        return $this->version;
133    }
134
135    /**
136     * Get the time-to-live for an item in cache
137     *
138     * @param  string $id
139     * @param  int    $default
140     * @return int
141     */
142    public function getItemTtl(string $id, int $default = 0): int
143    {
144        $cacheValue = $this->memcached->get($this->key($id));
145        $ttl        = $default;
146
147        if (is_array($cacheValue) && array_key_exists('ttl', $cacheValue)) {
148            $ttl = $cacheValue['ttl'];
149        }
150
151        return $ttl;
152    }
153
154    /**
155     * Save an item to cache
156     *
157     * @param  string $id
158     * @param  mixed  $value
159     * @param  ?int   $ttl
160     * @return Memcached
161     */
162    public function saveItem(string $id, mixed $value, ?int $ttl = null): Memcached
163    {
164        $cacheValue = [
165            'start' => $this->clock->now(),
166            'ttl'   => ($ttl !== null) ? $ttl : $this->ttl,
167            'value' => $value
168        ];
169
170        $this->memcached->set($this->key($id), $cacheValue, $cacheValue['ttl']);
171
172        return $this;
173    }
174
175    /**
176     * Get an item from cache
177     *
178     * @param  string $id
179     * @param  mixed  $default
180     * @return mixed
181     */
182    public function getItem(string $id, mixed $default = false): mixed
183    {
184        $cacheValue = $this->memcached->get($this->key($id));
185        $value      = $default;
186
187        if (is_array($cacheValue) && array_key_exists('start', $cacheValue) &&
188            array_key_exists('ttl', $cacheValue) && array_key_exists('value', $cacheValue)) {
189            if ($this->isFresh($cacheValue)) {
190                $value = $cacheValue['value'];
191            } else {
192                $this->deleteItem($id);
193            }
194        }
195
196        return $value;
197    }
198
199    /**
200     * Determine if the item exist in cache
201     *
202     * @param  string $id
203     * @return bool
204     */
205    public function hasItem(string $id): bool
206    {
207        return ($this->getItem($id) !== false);
208    }
209
210    /**
211     * Delete a value in cache
212     *
213     * @param  string $id
214     * @return Memcached
215     */
216    public function deleteItem(string $id): Memcached
217    {
218        $this->memcached->delete($this->key($id));
219        return $this;
220    }
221
222    /**
223     * Clear all stored values from cache
224     *
225     * @return Memcached
226     */
227    public function clear(): Memcached
228    {
229        $this->memcached->set($this->versionKey(), $this->resolveVersion() + 1, 0);
230        return $this;
231    }
232
233    /**
234     * Destroy cache resource
235     *
236     * @return Memcached
237     */
238    public function destroy(): Memcached
239    {
240        $this->clear();
241        $this->memcached = null;
242        return $this;
243    }
244
245    /**
246     * Atomically increment a counter in cache, creating it at $initial if it doesn't exist
247     *
248     * Stored as a raw scalar, bypassing the start/ttl/value envelope used by saveItem()/getItem() entirely
249     * — a counter key and a saveItem()-managed key are two incompatible storage formats on this adapter,
250     * and a counter is not readable via getItem(). Requires the binary protocol (enabled in the
251     * constructor) — the default ASCII protocol silently ignores a non-default initial value. $ttl is
252     * honored only when the counter is first created; a later call does not refresh an existing counter's
253     * expiry.
254     *
255     * Deviation from the original plan: Memcached::increment() alone, when creating a new counter, seeds
256     * it at $initial only and silently discards $amount (verified empirically: incrementItem('x', 5, 100)
257     * on a fresh key yields 100, not 105) — unlike Apc/Redis, where the equivalent single primitive folds
258     * both together. It also leaves the value stored as a raw ASCII wire-protocol counter, which
259     * Memcached::get() then returns as a string, not an int. So, mirroring the Apc adapter's
260     * apcu_add()+apcu_inc() pattern, this seeds via Memcached::add() (a no-op if the key already exists)
261     * and then unconditionally applies Memcached::increment() on top — this also happens to make
262     * Memcached::get() return a proper int thereafter, since add() stores through the normal serializer.
263     *
264     * @param  string $id
265     * @param  int    $amount
266     * @param  int    $initial
267     * @param  ?int   $ttl
268     * @throws Exception
269     * @return int
270     */
271    public function incrementItem(string $id, int $amount = 1, int $initial = 0, ?int $ttl = null): int
272    {
273        $key = $this->key($id);
274        $ttl = ($ttl !== null) ? $ttl : $this->ttl;
275
276        $this->memcached->add($key, $initial, $ttl);
277        $result = $this->memcached->increment($key, $amount, $initial, $ttl);
278
279        if ($result === false) {
280            throw new Exception('Error: The value at that key is not numeric.');
281        }
282
283        return $result;
284    }
285
286    /**
287     * Atomically decrement a counter in cache, creating it at $initial if it doesn't exist
288     *
289     * Stored as a raw scalar, bypassing the start/ttl/value envelope used by saveItem()/getItem() entirely
290     * — a counter key and a saveItem()-managed key are two incompatible storage formats on this adapter,
291     * and a counter is not readable via getItem(). Unlike Redis/Apc, Memcached::decrement() clamps its
292     * result at 0 (unsigned 64-bit wire protocol) rather than going negative — a documented, accepted
293     * quirk of this adapter specifically.
294     *
295     * Deviation from the original plan: seeds via Memcached::add() before Memcached::decrement(), for the
296     * same reasons as incrementItem() above (see that method's docblock) — symmetry, and so a
297     * newly-created counter reflects $amount applied on top of $initial rather than $initial alone.
298     *
299     * @param  string $id
300     * @param  int    $amount
301     * @param  int    $initial
302     * @param  ?int   $ttl
303     * @throws Exception
304     * @return int
305     */
306    public function decrementItem(string $id, int $amount = 1, int $initial = 0, ?int $ttl = null): int
307    {
308        $key = $this->key($id);
309        $ttl = ($ttl !== null) ? $ttl : $this->ttl;
310
311        $this->memcached->add($key, $initial, $ttl);
312        $result = $this->memcached->decrement($key, $amount, $initial, $ttl);
313
314        if ($result === false) {
315            throw new Exception('Error: The value at that key is not numeric.');
316        }
317
318        return $result;
319    }
320
321    /**
322     * Fetch the raw version value from memcached, or false if it isn't set
323     *
324     * @param  string $key
325     * @return mixed
326     */
327    protected function fetchVersion(string $key): mixed
328    {
329        return $this->memcached->get($key);
330    }
331
332}