Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
Session
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
11 / 11
23
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getItemTtl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 saveItem
100.00% covered (success)
100.00%
6 / 6
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
3
 hasItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 deleteItem
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clear
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 destroy
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 incrementItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 decrementItem
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 key
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 * Session adapter cache 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 Session extends AbstractAdapter
30{
31
32    /**
33     * Cache namespace
34     * @var string
35     */
36    protected string $namespace = 'pop_cache';
37
38    /**
39     * Constructor
40     *
41     * Instantiate the cache session object
42     *
43     * @param  int $ttl
44     * @param  string $namespace
45     * @param  Clock\ClockInterface $clock
46     */
47    public function __construct(
48        int $ttl = 0, string $namespace = 'pop_cache', Clock\ClockInterface $clock = new Clock\SystemClock()
49    )
50    {
51        parent::__construct($ttl, $clock);
52        $this->namespace = $namespace;
53        if (session_id() == '') {
54            session_start();
55        }
56        if (!isset($_SESSION['_POP_CACHE_'])) {
57            $_SESSION['_POP_CACHE_'] = [];
58        }
59    }
60
61    /**
62     * Get the time-to-live for an item in cache
63     *
64     * @param  string $id
65     * @param  int    $default
66     * @return int
67     */
68    public function getItemTtl(string $id, int $default = 0): int
69    {
70        $ttl = $default;
71        $key = $this->key($id);
72
73        if (isset($_SESSION['_POP_CACHE_'][$key])) {
74            $cacheValue = unserialize($_SESSION['_POP_CACHE_'][$key], ['allowed_classes' => false]);
75            $ttl        = $cacheValue['ttl'];
76        }
77
78        return $ttl;
79    }
80
81    /**
82     * Save an item to cache
83     *
84     * @param  string $id
85     * @param  mixed  $value
86     * @param  ?int   $ttl
87     * @return Session
88     */
89    public function saveItem(string $id, mixed $value, ?int $ttl = null): Session
90    {
91        $_SESSION['_POP_CACHE_'][$this->key($id)] = serialize([
92            'start' => $this->clock->now(),
93            'ttl'   => ($ttl !== null) ? $ttl : $this->ttl,
94            'value' => $value
95        ]);
96        return $this;
97    }
98
99    /**
100     * Get an item from cache
101     *
102     * @param  string $id
103     * @param  mixed  $default
104     * @return mixed
105     */
106    public function getItem(string $id, mixed $default = false): mixed
107    {
108        $value = $default;
109        $key   = $this->key($id);
110
111        if (isset($_SESSION['_POP_CACHE_'][$key])) {
112            $cacheValue = unserialize($_SESSION['_POP_CACHE_'][$key], ['allowed_classes' => false]);
113            if ($this->isFresh($cacheValue)) {
114                $value = $cacheValue['value'];
115            } else {
116                $this->deleteItem($id);
117            }
118        }
119
120        return $value;
121    }
122
123    /**
124     * Determine if the item exist in cache
125     *
126     * @param  string $id
127     * @return bool
128     */
129    public function hasItem(string $id): bool
130    {
131        $result = false;
132        $key    = $this->key($id);
133
134        if (isset($_SESSION['_POP_CACHE_'][$key])) {
135            $cacheValue = unserialize($_SESSION['_POP_CACHE_'][$key], ['allowed_classes' => false]);
136            $result = $this->isFresh($cacheValue);
137        }
138
139        return $result;
140    }
141
142    /**
143     * Delete a value in cache
144     *
145     * @param  string $id
146     * @return Session
147     */
148    public function deleteItem(string $id): Session
149    {
150        $key = $this->key($id);
151        if (isset($_SESSION['_POP_CACHE_'][$key])) {
152            unset($_SESSION['_POP_CACHE_'][$key]);
153        }
154        return $this;
155    }
156
157    /**
158     * Clear all stored values from cache
159     *
160     * @return Session
161     */
162    public function clear(): Session
163    {
164        $prefix = $this->namespace . ':';
165        foreach (array_keys($_SESSION['_POP_CACHE_']) as $k) {
166            if (str_starts_with((string)$k, $prefix)) {
167                unset($_SESSION['_POP_CACHE_'][$k]);
168            }
169        }
170        return $this;
171    }
172
173    /**
174     * Destroy cache resource
175     *
176     * @return Session
177     */
178    public function destroy(): Session
179    {
180        $_SESSION = null;
181        session_unset();
182        session_destroy();
183        return $this;
184    }
185
186    /**
187     * Atomically increment a counter in cache, creating it at $initial if it doesn't exist
188     *
189     * Non-atomic read-modify-write through the same start/ttl/value envelope used by saveItem()/getItem() —
190     * Session has no native atomic primitive, so a counter here is an ordinary cached integer, fully
191     * interoperable with getItem()/hasItem()/deleteItem().
192     *
193     * @param  string $id
194     * @param  int    $amount
195     * @param  int    $initial
196     * @param  ?int   $ttl
197     * @throws Exception
198     * @return int
199     */
200    public function incrementItem(string $id, int $amount = 1, int $initial = 0, ?int $ttl = null): int
201    {
202        $current = $this->getItem($id, $initial);
203
204        if (!is_int($current)) {
205            throw new Exception('Error: The value at that key is not numeric.');
206        }
207
208        $value = $current + $amount;
209        $this->saveItem($id, $value, $ttl);
210
211        return $value;
212    }
213
214    /**
215     * Atomically decrement a counter in cache, creating it at $initial if it doesn't exist
216     *
217     * Non-atomic read-modify-write through the same start/ttl/value envelope used by saveItem()/getItem() —
218     * Session has no native atomic primitive, so a counter here is an ordinary cached integer, fully
219     * interoperable with getItem()/hasItem()/deleteItem().
220     *
221     * @param  string $id
222     * @param  int    $amount
223     * @param  int    $initial
224     * @param  ?int   $ttl
225     * @throws Exception
226     * @return int
227     */
228    public function decrementItem(string $id, int $amount = 1, int $initial = 0, ?int $ttl = null): int
229    {
230        $current = $this->getItem($id, $initial);
231
232        if (!is_int($current)) {
233            throw new Exception('Error: The value at that key is not numeric.');
234        }
235
236        $value = $current - $amount;
237        $this->saveItem($id, $value, $ttl);
238
239        return $value;
240    }
241
242    /**
243     * Build the namespaced storage key for an item id
244     *
245     * Unlike Apc/Memcached/Redis, this adapter's clear() deletes matching keys directly by prefix
246     * (a plain PHP array can be enumerated and filtered cheaply) rather than via generational
247     * versioning, so this key has no version segment — there's no backend-scan limitation here to
248     * work around.
249     *
250     * @param  string $id
251     * @return string
252     */
253    protected function key(string $id): string
254    {
255        return $this->namespace . ':' . sha1($id);
256    }
257}