Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.10% covered (success)
93.10%
81 / 87
88.89% covered (success)
88.89%
16 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Session
93.10% covered (success)
93.10%
81 / 87
88.89% covered (success)
88.89%
16 / 18
39.50
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
3
 getInstance
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 regenerateId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 init
50.00% covered (warning)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
4.12
 kill
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 setTimedValue
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setRequestValue
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 checkRequest
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 checkRequests
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 checkExpiration
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 checkExpirations
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 __set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __unset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Pop PHP Framework (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Session;
15
16/**
17 * Session class
18 *
19 * @category   Pop
20 * @package    Pop\Session
21 * @author     Nick Sagona, III <dev@noladev.com>
22 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
23 * @license    https://www.popphp.org/license     New BSD License
24 * @version    4.0.2
25 */
26class Session extends AbstractSession
27{
28
29    /**
30     * Instance of the session
31     * @var ?object
32     */
33    private static ?object $instance = null;
34
35    /**
36     * Session Name
37     * @var ?string
38     */
39    private ?string $sessionName = null;
40
41    /**
42     * Session ID
43     * @var ?string
44     */
45    private ?string $sessionId = null;
46
47    /**
48     * Constructor
49     *
50     * @param array $options
51     *
52     * Private method to instantiate the session object
53     */
54    private function __construct(array $options = [])
55    {
56        // Start a session and set the session id.
57        if (session_id() == '') {
58            if (!empty($options)) {
59                $sessionParams = session_get_cookie_params();
60                $lifetime      = $options['lifetime'] ?? $sessionParams['lifetime'];
61                $path          = $options['path']     ?? $sessionParams['lifetime'];
62                $domain        = $options['domain']   ?? $sessionParams['domain'];
63                $secure        = $options['secure']   ??  $sessionParams['secure'];
64                $httponly      = $options['httponly'] ??  $sessionParams['httponly'];
65                $sameSite      = $options['samesite'] ??  $sessionParams['samesite'];
66
67                session_set_cookie_params([
68                    'lifetime' => $lifetime,
69                    'path'     => $path,
70                    'domain'   => $domain,
71                    'secure'   => $secure,
72                    'httponly' => $httponly,
73                    'samesite' => $sameSite
74                ]);
75            }
76            session_start();
77            $this->sessionId   = session_id();
78            $this->sessionName = session_name();
79            $this->init();
80        }
81    }
82
83    /**
84     * Determine whether or not an instance of the session object exists already,
85     * and instantiate the object if it does not exist.
86     *
87     * @param  array $options
88     * @return Session
89     */
90    public static function getInstance(array $options = []): Session
91    {
92        if (null === self::$instance) {
93            self::$instance = new Session($options);
94        }
95
96        self::$instance->checkRequests();
97        self::$instance->checkExpirations();
98
99        return self::$instance;
100    }
101
102    /**
103     * Return the current the session name
104     *
105     * @return string
106     */
107    public function getName(): string
108    {
109        return $this->sessionName;
110    }
111
112    /**
113     * Return the current the session id
114     *
115     * @return string
116     */
117    public function getId(): string
118    {
119        return $this->sessionId;
120    }
121
122    /**
123     * Regenerate the session id
124     *
125     * @param  bool $deleteOldSession
126     * @return void
127     */
128    public function regenerateId(bool $deleteOldSession = true): void
129    {
130        session_regenerate_id($deleteOldSession);
131        $this->sessionId   = session_id();
132        $this->sessionName = session_name();
133    }
134
135    /**
136     * Init the session
137     *
138     * @return void
139     */
140    private function init(): void
141    {
142        if (!isset($_SESSION['_POP_SESSION_'])) {
143            $_SESSION['_POP_SESSION_'] = [
144                'requests'    => [],
145                'expirations' => []
146            ];
147        } else if (!isset($_SESSION['_POP_SESSION_']['requests'])) {
148            $_SESSION['_POP_SESSION_']['requests']    = [];
149            $_SESSION['_POP_SESSION_']['expirations'] = [];
150        } else {
151            $this->checkRequests();
152            $this->checkExpirations();
153        }
154    }
155
156    /**
157     * Destroy the session
158     *
159     * @return void
160     */
161    public function kill(): void
162    {
163        if (!empty($this->sessionName) && !empty($this->sessionId) &&
164            isset($_COOKIE[$this->sessionName]) && ($_COOKIE[$this->sessionName] == $this->sessionId)) {
165            setcookie($this->sessionName, $this->sessionId, time() - 3600);
166        }
167
168        $_SESSION = null;
169        session_unset();
170        session_destroy();
171        self::$instance    = null;
172        $this->sessionId   = null;
173        $this->sessionName = null;
174    }
175
176    /**
177     * Set a time-based value
178     *
179     * @param  string $key
180     * @param  mixed  $value
181     * @param  int    $expire
182     * @return Session
183     */
184    public function setTimedValue(string $key, mixed $value, int $expire = 300): Session
185    {
186        $_SESSION[$key] = $value;
187        $_SESSION['_POP_SESSION_']['expirations'][$key] = time() + (int)$expire;
188        return $this;
189    }
190
191    /**
192     * Set a request-based value
193     *
194     * @param  string $key
195     * @param  mixed  $value
196     * @param  int    $hops
197     * @return Session
198     */
199    public function setRequestValue(string $key, mixed $value, int $hops = 1): Session
200    {
201        $_SESSION[$key] = $value;
202        $_SESSION['_POP_SESSION_']['requests'][$key] = [
203            'current' => 0,
204            'limit'   => (int)$hops
205        ];
206        return $this;
207    }
208
209    /**
210     * Check the request-based session value
211     *
212     * @return void
213     */
214    private function checkRequest($key): void
215    {
216        if (isset($_SESSION['_POP_SESSION_']['requests'][$key])) {
217            $_SESSION['_POP_SESSION_']['requests'][$key]['current']++;
218            $current = $_SESSION['_POP_SESSION_']['requests'][$key]['current'];
219            $limit   = $_SESSION['_POP_SESSION_']['requests'][$key]['limit'];
220            if ($current > $limit) {
221                unset($_SESSION[$key]);
222                unset($_SESSION['_POP_SESSION_']['requests'][$key]);
223            }
224        }
225    }
226
227    /**
228     * Check the request-based session values
229     *
230     * @return void
231     */
232    private function checkRequests(): void
233    {
234        foreach ($_SESSION as $key => $value) {
235            $this->checkRequest($key);
236        }
237    }
238
239    /**
240     * Check the time-based session value
241     *
242     * @return void
243     */
244    private function checkExpiration($key): void
245    {
246        if (isset($_SESSION['_POP_SESSION_']['expirations'][$key]) &&
247            (time() > $_SESSION['_POP_SESSION_']['expirations'][$key])) {
248            unset($_SESSION[$key]);
249            unset($_SESSION['_POP_SESSION_']['expirations'][$key]);
250        }
251    }
252
253    /**
254     * Check the time-based session values
255     *
256     * @return void
257     */
258    private function checkExpirations(): void
259    {
260        foreach ($_SESSION as $key => $value) {
261            $this->checkExpiration($key);
262        }
263    }
264
265    /**
266     * Get the session values as an array
267     *
268     * @return array
269     */
270    public function toArray(): array
271    {
272        $session = $_SESSION;
273
274        if (isset($session['_POP_SESSION_'])) {
275            unset($session['_POP_SESSION_']);
276        }
277
278        return $session;
279    }
280
281    /**
282     * Set a property in the session object that is linked to the $_SESSION global variable
283     *
284     * @param  string $name
285     * @param  mixed  $value
286     * @throws Exception
287     * @return void
288     */
289    public function __set(string $name, mixed $value): void
290    {
291        if ($name == '_POP_SESSION_') {
292            throw new Exception("Error: Cannot use the reserved name '_POP_SESSION_'.");
293        }
294        $_SESSION[$name] = $value;
295    }
296
297    /**
298     * Get method to return the value of the $_SESSION global variable
299     *
300     * @param  string $name
301     * @return mixed
302     */
303    public function __get(string $name): mixed
304    {
305        return (($name !== '_POP_SESSION_') && isset($_SESSION[$name])) ? $_SESSION[$name] : null;
306    }
307
308    /**
309     * Return the isset value of the $_SESSION global variable
310     *
311     * @param  string $name
312     * @return bool
313     */
314    public function __isset(string $name): bool
315    {
316        return (($name !== '_POP_SESSION_') && isset($_SESSION[$name]));
317    }
318
319    /**
320     * Unset the $_SESSION global variable
321     *
322     * @param  string $name
323     * @throws Exception
324     * @return void
325     */
326    public function __unset(string $name): void
327    {
328        if ($name == '_POP_SESSION_') {
329            throw new Exception("Error: Cannot use the reserved name '_POP_SESSION_'.");
330        }
331
332        $_SESSION[$name] = null;
333        unset($_SESSION[$name]);
334    }
335
336}