Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
51 / 51
77.50% covered (warning)
77.50%
31 / 40
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
Session
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
51 / 51
77.50% covered (warning)
77.50%
31 / 40
100.00% covered (success)
100.00%
16 / 16
48.95
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 start
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
11 / 11
55.56% covered (warning)
55.56%
5 / 9
100.00% covered (success)
100.00%
1 / 1
11.30
 destroy
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
8 / 8
37.50% covered (danger)
37.50%
3 / 8
100.00% covered (success)
100.00%
1 / 1
11.10
 name
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 id
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 all
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
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
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 pull
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 set
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 remove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 active
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 close
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 regenerate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 assertActive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Session;
6
7use SessionHandlerInterface;
8
9/** @api */
10class Session
11{
12    private const array DEFAULT_OPTIONS = [
13        'cache_limiter' => 'nocache',
14        'cookie_httponly' => true,
15        'cookie_samesite' => 'Lax',
16        'cookie_secure' => true,
17        'use_only_cookies' => true,
18        'use_strict_mode' => true,
19        'use_trans_sid' => false,
20    ];
21
22    protected readonly array $options;
23
24    /** @psalm-suppress UnusedProperty Used by the $flash property hook. */
25    private ?Flash $flashInstance = null;
26
27    /** @psalm-suppress UnusedProperty Used by the $csrf property hook. */
28    private ?Csrf $csrfInstance = null;
29
30    /** @psalm-suppress UnusedProperty Used by the $uri property hook. */
31    private ?Uri $uriInstance = null;
32
33    /** @psalm-suppress UnusedProperty Used by helper property hooks. */
34    private readonly Contract\Helpers $helpers;
35
36    /** @psalm-suppress PropertyNotSetInConstructor Virtual property backed by a get hook. */
37    public Flash $flash {
38        get => $this->flashInstance ??= $this->helpers->flash($this);
39    }
40
41    /** @psalm-suppress PropertyNotSetInConstructor Virtual property backed by a get hook. */
42    public Csrf $csrf {
43        get => $this->csrfInstance ??= $this->helpers->csrf($this);
44    }
45
46    /** @psalm-suppress PropertyNotSetInConstructor Virtual property backed by a get hook. */
47    public Uri $uri {
48        get => $this->uriInstance ??= $this->helpers->uri($this);
49    }
50
51    public function __construct(
52        array $options = [],
53        protected readonly string $name = '',
54        protected readonly ?SessionHandlerInterface $handler = null,
55        Contract\Helpers $helpers = new Helpers(),
56    ) {
57        $this->options = array_replace(self::DEFAULT_OPTIONS, $options);
58        $this->helpers = $helpers;
59    }
60
61    public function start(): void
62    {
63        if (session_status() !== PHP_SESSION_NONE) {
64            return;
65        }
66
67        if (headers_sent($file, $line)) {
68            // Requires sent headers, which the test suite cannot trigger reliably.
69            // @codeCoverageIgnoreStart
70            throw new RuntimeException(
71                __METHOD__ . 'Session started after headers sent. File: ' . $file . ' line: ' . $line,
72            );
73
74            // @codeCoverageIgnoreEnd
75        }
76
77        if ($this->name) {
78            session_name($this->name);
79        }
80
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
82            throw new RuntimeException('Session handler setup failed');
83        }
84
85        if (!session_start($this->options)) {
86            // session_start() only returns false after PHP accepts the setup but fails internally;
87            // forcing that path would make the test process depend on unstable runtime state.
88            throw new RuntimeException(__METHOD__ . 'session_start failed.'); // @codeCoverageIgnore
89        }
90    }
91
92    public function destroy(): void
93    {
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
103                /** @psalm-suppress InvalidArrayOffset PHP 8.5 exposes partitioned cookies. */
104                $partitioned = (bool) ($params['partitioned'] ?? false);
105                $options = [
106                    'expires' => time() - 42000,
107                    'path' => (string) $params['path'],
108                    'secure' => (bool) $params['secure'],
109                    'httponly' => (bool) $params['httponly'],
110                    'samesite' => (string) $params['samesite'],
111                    'partitioned' => $partitioned,
112                ];
113
114                $domain = (string) $params['domain'];
115                if ($domain !== '') {
116                    $options['domain'] = $domain;
117                }
118
119                setcookie($name, '', $options);
120            }
121        }
122
123        if (!session_destroy()) {
124            throw new RuntimeException('Session destroy failed');
125        }
126    }
127
128    public function name(): string
129    {
130        $name = session_name();
131        if ($name === false) {
132            throw new RuntimeException('Session name not available');
133        }
134
135        return $name;
136    }
137
138    public function id(): string
139    {
140        $id = session_id();
141        if ($id === false) {
142            throw new RuntimeException('Session id not available');
143        }
144
145        return $id;
146    }
147
148    /** @return array<array-key, mixed> */
149    public function all(): array
150    {
151        $this->assertActive();
152
153        /**
154         * @var array<array-key, mixed> $session
155         * @mago-expect lint:inline-variable-return Keep the explicit type for `$_SESSION`.
156         */
157        $session = $_SESSION;
158
159        return $session;
160    }
161
162    public function clear(): void
163    {
164        $this->assertActive();
165
166        session_unset();
167    }
168
169    /** @param non-empty-string $key */
170    public function get(string $key, mixed $default = null): mixed
171    {
172        $this->assertActive();
173
174        if ($this->has($key)) {
175            return $_SESSION[$key];
176        }
177
178        if (func_num_args() > 1) {
179            return $default;
180        }
181
182        throw new OutOfBoundsException(
183            "The session key '{$key}' does not exist",
184        );
185    }
186
187    /**
188     * @psalm-suppress MixedAssignment
189     * @param non-empty-string $key
190     */
191    public function pull(string $key, mixed $default = null): mixed
192    {
193        $this->assertActive();
194
195        if ($this->has($key)) {
196            $value = $_SESSION[$key];
197            unset($_SESSION[$key]);
198
199            return $value;
200        }
201
202        if (func_num_args() > 1) {
203            return $default;
204        }
205
206        throw new OutOfBoundsException(
207            "The session key '{$key}' does not exist",
208        );
209    }
210
211    /**
212     * @psalm-suppress MixedAssignment
213     * @param non-empty-string $key
214     * */
215    public function set(string $key, mixed $value): void
216    {
217        $this->assertActive();
218
219        $_SESSION[$key] = $value;
220    }
221
222    /** @param non-empty-string $key */
223    public function has(string $key): bool
224    {
225        $this->assertActive();
226
227        return ($_SESSION[$key] ?? null) !== null;
228    }
229
230    /** @param non-empty-string $key */
231    public function remove(string $key): void
232    {
233        $this->assertActive();
234
235        unset($_SESSION[$key]);
236    }
237
238    public function active(): bool
239    {
240        return session_status() === PHP_SESSION_ACTIVE;
241    }
242
243    public function close(): void
244    {
245        $this->assertActive();
246
247        if (!session_write_close()) {
248            throw new RuntimeException('Session close failed');
249        }
250    }
251
252    public function regenerate(): void
253    {
254        $this->assertActive();
255
256        if (!session_regenerate_id(true)) {
257            throw new RuntimeException('Session id regeneration failed');
258        }
259    }
260
261    private function assertActive(): void
262    {
263        if (!$this->active()) {
264            throw new RuntimeException('Session not started');
265        }
266    }
267}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Session->$csrf::get
43        get => $this->csrfInstance ??= $this->helpers->csrf($this);
Session->$flash::get
38        get => $this->flashInstance ??= $this->helpers->flash($this);
Session->$uri::get
48        get => $this->uriInstance ??= $this->helpers->uri($this);
Session->__construct
52        array $options = [],
53        protected readonly string $name = '',
54        protected readonly ?SessionHandlerInterface $handler = null,
55        Contract\Helpers $helpers = new Helpers(),
56    ) {
57        $this->options = array_replace(self::DEFAULT_OPTIONS, $options);
58        $this->helpers = $helpers;
59    }
Session->active
240        return session_status() === PHP_SESSION_ACTIVE;
241    }
Session->all
151        $this->assertActive();
152
153        /**
154         * @var array<array-key, mixed> $session
155         * @mago-expect lint:inline-variable-return Keep the explicit type for `$_SESSION`.
156         */
157        $session = $_SESSION;
158
159        return $session;
160    }
Session->assertActive
263        if (!$this->active()) {
 
264            throw new RuntimeException('Session not started');
263        if (!$this->active()) {
 
266    }
Session->clear
164        $this->assertActive();
165
166        session_unset();
167    }
Session->close
245        $this->assertActive();
246
247        if (!session_write_close()) {
 
248            throw new RuntimeException('Session close failed');
245        $this->assertActive();
246
247        if (!session_write_close()) {
 
250    }
Session->destroy
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
 
104                $partitioned = (bool) ($params['partitioned'] ?? false);
105                $options = [
106                    'expires' => time() - 42000,
107                    'path' => (string) $params['path'],
108                    'secure' => (bool) $params['secure'],
109                    'httponly' => (bool) $params['httponly'],
110                    'samesite' => (string) $params['samesite'],
111                    'partitioned' => $partitioned,
112                ];
113
114                $domain = (string) $params['domain'];
115                if ($domain !== '') {
 
116                    $options['domain'] = $domain;
117                }
118
119                setcookie($name, '', $options);
 
119                setcookie($name, '', $options);
120            }
121        }
122
123        if (!session_destroy()) {
 
123        if (!session_destroy()) {
 
124            throw new RuntimeException('Session destroy failed');
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
 
104                $partitioned = (bool) ($params['partitioned'] ?? false);
105                $options = [
106                    'expires' => time() - 42000,
107                    'path' => (string) $params['path'],
108                    'secure' => (bool) $params['secure'],
109                    'httponly' => (bool) $params['httponly'],
110                    'samesite' => (string) $params['samesite'],
111                    'partitioned' => $partitioned,
112                ];
113
114                $domain = (string) $params['domain'];
115                if ($domain !== '') {
 
116                    $options['domain'] = $domain;
117                }
118
119                setcookie($name, '', $options);
 
119                setcookie($name, '', $options);
120            }
121        }
122
123        if (!session_destroy()) {
 
123        if (!session_destroy()) {
 
126    }
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
 
104                $partitioned = (bool) ($params['partitioned'] ?? false);
105                $options = [
106                    'expires' => time() - 42000,
107                    'path' => (string) $params['path'],
108                    'secure' => (bool) $params['secure'],
109                    'httponly' => (bool) $params['httponly'],
110                    'samesite' => (string) $params['samesite'],
111                    'partitioned' => $partitioned,
112                ];
113
114                $domain = (string) $params['domain'];
115                if ($domain !== '') {
 
119                setcookie($name, '', $options);
120            }
121        }
122
123        if (!session_destroy()) {
 
123        if (!session_destroy()) {
 
124            throw new RuntimeException('Session destroy failed');
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
 
104                $partitioned = (bool) ($params['partitioned'] ?? false);
105                $options = [
106                    'expires' => time() - 42000,
107                    'path' => (string) $params['path'],
108                    'secure' => (bool) $params['secure'],
109                    'httponly' => (bool) $params['httponly'],
110                    'samesite' => (string) $params['samesite'],
111                    'partitioned' => $partitioned,
112                ];
113
114                $domain = (string) $params['domain'];
115                if ($domain !== '') {
 
119                setcookie($name, '', $options);
120            }
121        }
122
123        if (!session_destroy()) {
 
123        if (!session_destroy()) {
 
126    }
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
 
123        if (!session_destroy()) {
 
124            throw new RuntimeException('Session destroy failed');
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
100            $params = session_get_cookie_params();
101            $name = session_name();
102            if ($name !== false) {
 
123        if (!session_destroy()) {
 
126    }
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
123        if (!session_destroy()) {
 
124            throw new RuntimeException('Session destroy failed');
94        $this->assertActive();
95
96        session_unset();
97
98        $useCookies = ini_get('session.use_cookies');
99        if ($useCookies === '1') {
 
123        if (!session_destroy()) {
 
126    }
Session->get
170    public function get(string $key, mixed $default = null): mixed
171    {
172        $this->assertActive();
173
174        if ($this->has($key)) {
 
175            return $_SESSION[$key];
170    public function get(string $key, mixed $default = null): mixed
171    {
172        $this->assertActive();
173
174        if ($this->has($key)) {
 
178        if (func_num_args() > 1) {
 
179            return $default;
170    public function get(string $key, mixed $default = null): mixed
171    {
172        $this->assertActive();
173
174        if ($this->has($key)) {
 
178        if (func_num_args() > 1) {
 
182        throw new OutOfBoundsException(
183            "The session key '{$key}' does not exist",
184        );
185    }
Session->has
223    public function has(string $key): bool
224    {
225        $this->assertActive();
226
227        return ($_SESSION[$key] ?? null) !== null;
228    }
Session->id
140        $id = session_id();
141        if ($id === false) {
 
142            throw new RuntimeException('Session id not available');
140        $id = session_id();
141        if ($id === false) {
 
145        return $id;
146    }
Session->name
130        $name = session_name();
131        if ($name === false) {
 
132            throw new RuntimeException('Session name not available');
130        $name = session_name();
131        if ($name === false) {
 
135        return $name;
136    }
Session->pull
191    public function pull(string $key, mixed $default = null): mixed
192    {
193        $this->assertActive();
194
195        if ($this->has($key)) {
 
196            $value = $_SESSION[$key];
197            unset($_SESSION[$key]);
198
199            return $value;
191    public function pull(string $key, mixed $default = null): mixed
192    {
193        $this->assertActive();
194
195        if ($this->has($key)) {
 
202        if (func_num_args() > 1) {
 
203            return $default;
191    public function pull(string $key, mixed $default = null): mixed
192    {
193        $this->assertActive();
194
195        if ($this->has($key)) {
 
202        if (func_num_args() > 1) {
 
206        throw new OutOfBoundsException(
207            "The session key '{$key}' does not exist",
208        );
209    }
Session->regenerate
254        $this->assertActive();
255
256        if (!session_regenerate_id(true)) {
 
257            throw new RuntimeException('Session id regeneration failed');
254        $this->assertActive();
255
256        if (!session_regenerate_id(true)) {
 
259    }
Session->remove
231    public function remove(string $key): void
232    {
233        $this->assertActive();
234
235        unset($_SESSION[$key]);
236    }
Session->set
215    public function set(string $key, mixed $value): void
216    {
217        $this->assertActive();
218
219        $_SESSION[$key] = $value;
220    }
Session->start
63        if (session_status() !== PHP_SESSION_NONE) {
 
64            return;
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
78            session_name($this->name);
79        }
80
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
82            throw new RuntimeException('Session handler setup failed');
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
78            session_name($this->name);
79        }
80
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
85        if (!session_start($this->options)) {
 
90    }
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
78            session_name($this->name);
79        }
80
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
82            throw new RuntimeException('Session handler setup failed');
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
78            session_name($this->name);
79        }
80
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
85        if (!session_start($this->options)) {
 
90    }
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
82            throw new RuntimeException('Session handler setup failed');
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
85        if (!session_start($this->options)) {
 
90    }
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
82            throw new RuntimeException('Session handler setup failed');
63        if (session_status() !== PHP_SESSION_NONE) {
 
67        if (headers_sent($file, $line)) {
 
77        if ($this->name) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
81        if ($this->handler && !session_set_save_handler($this->handler, true)) {
 
85        if (!session_start($this->options)) {
 
90    }