Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
74 / 74
100.00% covered (success)
100.00%
17 / 17
CRAP
100.00% covered (success)
100.00%
1 / 1
Session
100.00% covered (success)
100.00%
74 / 74
100.00% covered (success)
100.00%
17 / 17
37
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 getInstance
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setHandler
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 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
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 kill
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 close
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
 sweep
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 toArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __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
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\Session;
16
17use SessionHandlerInterface;
18
19/**
20 * Session class
21 *
22 * @category   Pop
23 * @package    Pop\Session
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 AbstractSession
30{
31
32    /**
33     * Instance of the session
34     * @var ?object
35     */
36    private static ?object $instance = null;
37
38    /**
39     * Custom session save handler
40     * @var ?SessionHandlerInterface
41     */
42    private static ?SessionHandlerInterface $handler = null;
43
44    /**
45     * Session Name
46     * @var ?string
47     */
48    private ?string $sessionName = null;
49
50    /**
51     * Session ID
52     * @var ?string
53     */
54    private ?string $sessionId = null;
55
56    /**
57     * Constructor
58     *
59     * @param array $options
60     *
61     * Private method to instantiate the session object
62     */
63    private function __construct(array $options = [])
64    {
65        // Start a session if one isn't already active.
66        if (session_id() == '') {
67            $sessionParams = session_get_cookie_params();
68            $strictMode    = $options['strict_mode'] ?? true;
69
70            session_set_cookie_params([
71                'lifetime' => $options['lifetime'] ?? $sessionParams['lifetime'],
72                'path'     => $options['path']     ?? $sessionParams['path'],
73                'domain'   => $options['domain']   ?? $sessionParams['domain'],
74                'secure'   => $options['secure']   ?? $sessionParams['secure'],
75                'httponly' => $options['httponly'] ?? true,
76                'samesite' => $options['samesite'] ?? (ini_get('session.cookie_samesite') ?: 'Lax')
77            ]);
78
79            if (self::$handler !== null) {
80                session_set_save_handler(self::$handler, true);
81            }
82
83            session_start(['use_strict_mode' => $strictMode ? '1' : '0']);
84        }
85
86        $this->sessionId   = session_id();
87        $this->sessionName = session_name();
88        $this->init();
89    }
90
91    /**
92     * Determine whether or not an instance of the session object exists already,
93     * and instantiate the object if it does not exist.
94     *
95     * @param  array $options
96     * @return Session
97     */
98    public static function getInstance(array $options = []): Session
99    {
100        if (null === self::$instance) {
101            self::$instance = new Session($options);
102        } else {
103            self::$instance->sweep();
104        }
105
106        return self::$instance;
107    }
108
109    /**
110     * Set a custom session save handler
111     *
112     * Must be called before the first Session::getInstance() call, and before any
113     * other code has called session_start() â€” a save handler has no effect once a
114     * session is already active.
115     *
116     * @param  SessionHandlerInterface $handler
117     * @throws Exception
118     * @return void
119     */
120    public static function setHandler(SessionHandlerInterface $handler): void
121    {
122        if ((self::$instance !== null) || (session_status() === PHP_SESSION_ACTIVE)) {
123            throw new Exception("Error: Cannot set the session handler after the session has already started.");
124        }
125        self::$handler = $handler;
126    }
127
128    /**
129     * Return the current the session name
130     *
131     * @return string
132     */
133    public function getName(): string
134    {
135        return $this->sessionName;
136    }
137
138    /**
139     * Return the current the session id
140     *
141     * @return string
142     */
143    public function getId(): string
144    {
145        return $this->sessionId;
146    }
147
148    /**
149     * Regenerate the session id
150     *
151     * @param  bool $deleteOldSession
152     * @return void
153     */
154    public function regenerateId(bool $deleteOldSession = true): void
155    {
156        session_regenerate_id($deleteOldSession);
157        $this->sessionId   = session_id();
158        $this->sessionName = session_name();
159    }
160
161    /**
162     * Init the session
163     *
164     * @return void
165     */
166    private function init(): void
167    {
168        if (!isset($_SESSION['_POP_SESSION_'])) {
169            $_SESSION['_POP_SESSION_'] = [
170                'requests'    => [],
171                'expirations' => []
172            ];
173        } else if (!isset($_SESSION['_POP_SESSION_']['requests'])) {
174            $_SESSION['_POP_SESSION_']['requests']    = [];
175            $_SESSION['_POP_SESSION_']['expirations'] = [];
176        } else {
177            $this->sweep();
178        }
179    }
180
181    /**
182     * Destroy the session
183     *
184     * @return void
185     */
186    public function kill(): void
187    {
188        if (!empty($this->sessionName) && !empty($this->sessionId) &&
189            isset($_COOKIE[$this->sessionName]) && ($_COOKIE[$this->sessionName] == $this->sessionId)) {
190            setcookie($this->sessionName, $this->sessionId, time() - 3600);
191        }
192
193        if (session_status() === PHP_SESSION_ACTIVE) {
194            session_unset();
195            session_destroy();
196        }
197        $_SESSION = null;
198        self::$instance    = null;
199        $this->sessionId   = null;
200        $this->sessionName = null;
201    }
202
203    /**
204     * Close the session for writing, releasing the session lock without ending the session
205     *
206     * @return void
207     */
208    public function close(): void
209    {
210        session_write_close();
211    }
212
213    /**
214     * Set a time-based value
215     *
216     * @param  string $key
217     * @param  mixed  $value
218     * @param  int    $expire
219     * @return Session
220     */
221    public function setTimedValue(string $key, mixed $value, int $expire = 300): Session
222    {
223        $_SESSION[$key] = $value;
224        $_SESSION['_POP_SESSION_']['expirations'][$key] = time() + (int)$expire;
225        return $this;
226    }
227
228    /**
229     * Set a request-based value
230     *
231     * @param  string $key
232     * @param  mixed  $value
233     * @param  int    $hops
234     * @return Session
235     */
236    public function setRequestValue(string $key, mixed $value, int $hops = 1): Session
237    {
238        $_SESSION[$key] = $value;
239        $_SESSION['_POP_SESSION_']['requests'][$key] = [
240            'current' => 0,
241            'limit'   => (int)$hops
242        ];
243        return $this;
244    }
245
246    /**
247     * Manually check request-based and time-based values, removing any that have
248     * expired or exceeded their hop limit
249     *
250     * @return Session
251     */
252    public function sweep(): Session
253    {
254        if (isset($_SESSION['_POP_SESSION_'])) {
255            $this->checkRequestValues($_SESSION, $_SESSION['_POP_SESSION_']);
256            $this->checkExpirationValues($_SESSION, $_SESSION['_POP_SESSION_']);
257        }
258        return $this;
259    }
260
261    /**
262     * Get the session values as an array
263     *
264     * @return array
265     */
266    public function toArray(): array
267    {
268        $session = $_SESSION;
269        unset($session['_POP_SESSION_']);
270
271        return $session;
272    }
273
274    /**
275     * Set a property in the session object that is linked to the $_SESSION global variable
276     *
277     * @param  string $name
278     * @param  mixed  $value
279     * @throws Exception
280     * @return void
281     */
282    public function __set(string $name, mixed $value): void
283    {
284        if ($name == '_POP_SESSION_') {
285            throw new Exception("Error: Cannot use the reserved name '_POP_SESSION_'.");
286        }
287        $_SESSION[$name] = $value;
288    }
289
290    /**
291     * Get method to return the value of the $_SESSION global variable
292     *
293     * @param  string $name
294     * @return mixed
295     */
296    public function __get(string $name): mixed
297    {
298        return (($name !== '_POP_SESSION_') && isset($_SESSION[$name])) ? $_SESSION[$name] : null;
299    }
300
301    /**
302     * Return the isset value of the $_SESSION global variable
303     *
304     * @param  string $name
305     * @return bool
306     */
307    public function __isset(string $name): bool
308    {
309        return (($name !== '_POP_SESSION_') && isset($_SESSION[$name]));
310    }
311
312    /**
313     * Unset the $_SESSION global variable
314     *
315     * @param  string $name
316     * @throws Exception
317     * @return void
318     */
319    public function __unset(string $name): void
320    {
321        if ($name == '_POP_SESSION_') {
322            throw new Exception("Error: Cannot use the reserved name '_POP_SESSION_'.");
323        }
324
325        $_SESSION[$name] = null;
326        unset($_SESSION[$name]);
327    }
328
329}