• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aplus-framework / session / 14456058175

14 Apr 2025 09:19PM UTC coverage: 93.845% (+0.1%) from 93.732%
14456058175

push

github

natanfelles
Update user guide

991 of 1056 relevant lines covered (93.84%)

43.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.44
/src/Session.php
1
<?php declare(strict_types=1);
2
/*
3
 * This file is part of Aplus Framework Session Library.
4
 *
5
 * (c) Natan Felles <natanfelles@gmail.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
namespace Framework\Session;
11

12
use Framework\Session\Debug\SessionCollector;
13
use JetBrains\PhpStorm\Pure;
14
use LogicException;
15
use RuntimeException;
16

17
/**
18
 * Class Session.
19
 *
20
 * @package session
21
 */
22
class Session
23
{
24
    /**
25
     * @var array<string,mixed>
26
     */
27
    protected array $options = [];
28
    protected SaveHandler $saveHandler;
29
    protected SessionCollector $debugCollector;
30

31
    /**
32
     * Session constructor.
33
     *
34
     * @param array<string,int|string> $options
35
     * @param SaveHandler|null $handler
36
     */
37
    public function __construct(array $options = [], ?SaveHandler $handler = null)
38
    {
39
        $this->setOptions($options);
291✔
40
        if ($handler) {
291✔
41
            $this->saveHandler = $handler;
263✔
42
            \session_set_save_handler($handler);
263✔
43
        }
44
    }
45

46
    public function __destruct()
47
    {
48
        $this->stop();
282✔
49
    }
50

51
    public function __get(string $key) : mixed
52
    {
53
        return $this->get($key);
26✔
54
    }
55

56
    public function __set(string $key, mixed $value) : void
57
    {
58
        $this->set($key, $value);
44✔
59
    }
60

61
    public function __isset(string $key) : bool
62
    {
63
        return $this->has($key);
9✔
64
    }
65

66
    public function __unset(string $key) : void
67
    {
68
        $this->remove($key);
9✔
69
    }
70

71
    /**
72
     * @see http://php.net/manual/en/session.security.ini.php
73
     *
74
     * @param array<string,int|string> $custom
75
     */
76
    protected function setOptions(array $custom) : void
77
    {
78
        $serializer = \ini_get('session.serialize_handler');
291✔
79
        $serializer = $serializer === 'php' ? 'php_serialize' : $serializer;
291✔
80
        $secure = (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] === 'https')
291✔
81
            || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on');
291✔
82
        $default = [
291✔
83
            'name' => 'session_id',
291✔
84
            'serialize_handler' => $serializer,
291✔
85
            'cookie_domain' => '',
291✔
86
            'cookie_httponly' => 1,
291✔
87
            'cookie_lifetime' => 7200,
291✔
88
            'cookie_path' => '/',
291✔
89
            'cookie_samesite' => 'Strict',
291✔
90
            'cookie_secure' => $secure,
291✔
91
            'referer_check' => '',
291✔
92
            'use_cookies' => 1,
291✔
93
            'use_only_cookies' => 1,
291✔
94
            'use_strict_mode' => 1,
291✔
95
            'use_trans_sid' => 0,
291✔
96
            // used to auto-regenerate the session id:
97
            'auto_regenerate_maxlifetime' => 0,
291✔
98
            'auto_regenerate_destroy' => true,
291✔
99
            'set_cookie_permanent' => false,
291✔
100
        ];
291✔
101
        if (\PHP_VERSION_ID < 80400) {
291✔
102
            $default['sid_bits_per_character'] = 6;
291✔
103
            $default['sid_length'] = 48;
291✔
104
        }
105
        $this->options = $custom
291✔
106
            ? \array_replace($default, $custom)
282✔
107
            : $default;
25✔
108
    }
109

110
    /**
111
     * @param array<string,mixed> $custom
112
     *
113
     * @return array<string,mixed>
114
     */
115
    protected function getOptions(array $custom = []) : array
116
    {
117
        $options = $custom
290✔
118
            ? \array_replace($this->options, $custom)
8✔
119
            : $this->options;
290✔
120
        unset(
290✔
121
            $options['auto_regenerate_maxlifetime'],
290✔
122
            $options['auto_regenerate_destroy'],
290✔
123
            $options['set_cookie_permanent'],
290✔
124
        );
290✔
125
        return $options;
290✔
126
    }
127

128
    /**
129
     * @param array<string,int|string> $customOptions
130
     *
131
     * @throws LogicException if session was already active
132
     * @throws RuntimeException if session could not be started
133
     *
134
     * @return bool
135
     */
136
    public function start(array $customOptions = []) : bool
137
    {
138
        if ($this->isActive()) {
290✔
139
            throw new LogicException('Session was already active');
9✔
140
        }
141
        if (!@\session_start($this->getOptions($customOptions))) {
290✔
142
            $message = '';
5✔
143
            if (\error_get_last()) {
5✔
144
                $message = ': ' . \error_get_last()['message'];
5✔
145
            }
146
            throw new RuntimeException(
5✔
147
                'Session could not be started' . $message
5✔
148
            );
5✔
149
        }
150
        $time = \time();
290✔
151
        $this->setPermanentCookie($time);
290✔
152
        $this->autoRegenerate($time);
290✔
153
        $this->clearTemp($time);
290✔
154
        $this->clearFlash();
290✔
155
        return true;
290✔
156
    }
157

158
    /**
159
     * Make sure the session is active.
160
     *
161
     * If it is not active, it will start it.
162
     *
163
     * @throws RuntimeException if session could not be started
164
     *
165
     * @return bool
166
     */
167
    public function activate() : bool
168
    {
169
        if ($this->isActive()) {
9✔
170
            return true;
9✔
171
        }
172
        return $this->start();
9✔
173
    }
174

175
    /**
176
     * @param int $time
177
     *
178
     * @see https://www.php.net/manual/en/function.session-set-cookie-params.php#100657
179
     * @see https://stackoverflow.com/a/34252812/6027968
180
     */
181
    protected function setPermanentCookie(int $time) : void
182
    {
183
        $setCookie = (bool) $this->options['set_cookie_permanent'];
290✔
184
        if ($setCookie === false) {
290✔
185
            return;
290✔
186
        }
187
        $params = \session_get_cookie_params();
9✔
188
        \setcookie(
9✔
189
            \session_name(), // @phpstan-ignore-line
9✔
190
            \session_id(), // @phpstan-ignore-line
9✔
191
            [ // @phpstan-ignore-line
9✔
192
                'expires' => $time + $this->options['cookie_lifetime'],
9✔
193
                'path' => $params['path'],
9✔
194
                'domain' => $params['domain'],
9✔
195
                'secure' => $params['secure'],
9✔
196
                'httponly' => $params['httponly'],
9✔
197
                'samesite' => $params['samesite'],
9✔
198
            ]
9✔
199
        );
9✔
200
    }
201

202
    /**
203
     * Auto regenerate the session id.
204
     *
205
     * @param int $time
206
     *
207
     * @see https://owasp.org/www-community/attacks/Session_fixation
208
     */
209
    protected function autoRegenerate(int $time) : void
210
    {
211
        $maxlifetime = (int) $this->options['auto_regenerate_maxlifetime'];
290✔
212
        $isActive = $maxlifetime > 0;
290✔
213
        if (($isActive && empty($_SESSION['$']['regenerated_at']))
290✔
214
            || ($isActive && $_SESSION['$']['regenerated_at'] < ($time - $maxlifetime))
290✔
215
        ) {
216
            $this->regenerateId((bool) $this->options['auto_regenerate_destroy']);
10✔
217
        }
218
    }
219

220
    /**
221
     * Clears the Flash Data.
222
     */
223
    protected function clearFlash() : void
224
    {
225
        unset($_SESSION['$']['flash']['old']);
290✔
226
        if (isset($_SESSION['$']['flash']['new'])) {
290✔
227
            foreach ($_SESSION['$']['flash']['new'] as $key => $value) {
19✔
228
                $_SESSION['$']['flash']['old'][$key] = $value;
19✔
229
            }
230
        }
231
        unset($_SESSION['$']['flash']['new']);
290✔
232
        if (empty($_SESSION['$']['flash'])) {
290✔
233
            unset($_SESSION['$']['flash']);
290✔
234
        }
235
    }
236

237
    /**
238
     * Clears the Temp Data.
239
     *
240
     * @param int $time The max time to temp data survive
241
     */
242
    protected function clearTemp(int $time) : void
243
    {
244
        if (isset($_SESSION['$']['temp'])) {
290✔
245
            foreach ($_SESSION['$']['temp'] as $key => $value) {
9✔
246
                if ($value['ttl'] < $time) {
9✔
247
                    unset($_SESSION['$']['temp'][$key]);
9✔
248
                }
249
            }
250
        }
251
        if (empty($_SESSION['$']['temp'])) {
290✔
252
            unset($_SESSION['$']['temp']);
290✔
253
        }
254
    }
255

256
    /**
257
     * Tells if sessions are enabled, and one exists.
258
     *
259
     * @return bool
260
     */
261
    public function isActive() : bool
262
    {
263
        return \session_status() === \PHP_SESSION_ACTIVE;
291✔
264
    }
265

266
    /**
267
     * Destroys all data registered to a session.
268
     *
269
     * @return bool true on success or false on failure
270
     */
271
    public function destroy() : bool
272
    {
273
        if ($this->isActive()) {
281✔
274
            $destroyed = \session_destroy();
241✔
275
        }
276
        unset($_SESSION);
281✔
277
        return $destroyed ?? true;
281✔
278
    }
279

280
    /**
281
     * Sets a Cookie with the session name to be destroyed in the user-agent.
282
     *
283
     * @throws RuntimeException If it could not get the session name
284
     *
285
     * @return bool True if the Set-Cookie header was set to invalidate the
286
     * session cookie, false if output exists
287
     */
288
    public function destroyCookie() : bool
289
    {
290
        $name = \session_name();
9✔
291
        if ($name === false) {
9✔
292
            throw new RuntimeException('Could not get the session name');
×
293
        }
294
        $params = \session_get_cookie_params();
9✔
295
        // @phpstan-ignore-next-line
296
        return \setcookie($name, '', [
9✔
297
            'expires' => 0,
9✔
298
            'path' => $params['path'],
9✔
299
            'domain' => $params['domain'],
9✔
300
            'secure' => $params['secure'],
9✔
301
            'httponly' => $params['httponly'],
9✔
302
            'samesite' => $params['samesite'],
9✔
303
        ]);
9✔
304
    }
305

306
    /**
307
     * Write session data and end session.
308
     *
309
     * @return bool returns true on success or false on failure
310
     */
311
    public function stop() : bool
312
    {
313
        if ($this->isActive()) {
283✔
314
            $closed = \session_write_close();
122✔
315
        }
316
        return $closed ?? true;
283✔
317
    }
318

319
    /**
320
     * Discard session data changes and end session.
321
     *
322
     * @return bool returns true on success or false on failure
323
     */
324
    public function abort() : bool
325
    {
326
        if ($this->isActive()) {
9✔
327
            $aborted = \session_abort();
9✔
328
        }
329
        return $aborted ?? true;
9✔
330
    }
331

332
    /**
333
     * Tells if the session has an item.
334
     *
335
     * @param string $key The item key name
336
     *
337
     * @return bool True if it has, otherwise false
338
     */
339
    #[Pure]
340
    public function has(string $key) : bool
341
    {
342
        return isset($_SESSION[$key]);
9✔
343
    }
344

345
    /**
346
     * Gets one session item.
347
     *
348
     * @param string $key The item key name
349
     *
350
     * @return mixed The item value or null if no set
351
     */
352
    #[Pure]
353
    public function get(string $key) : mixed
354
    {
355
        return $_SESSION[$key] ?? null;
53✔
356
    }
357

358
    /**
359
     * Get all session items.
360
     *
361
     * @return array<mixed> The value of the $_SESSION global
362
     */
363
    #[Pure]
364
    public function getAll() : array
365
    {
366
        return $_SESSION;
27✔
367
    }
368

369
    /**
370
     * Get multiple session items.
371
     *
372
     * @param array<string> $keys An array of key item names
373
     *
374
     * @return array<string,mixed> An associative array with items keys and
375
     * values. Item not set will return as null.
376
     */
377
    #[Pure]
378
    public function getMulti(array $keys) : array
379
    {
380
        $items = [];
9✔
381
        foreach ($keys as $key) {
9✔
382
            $items[$key] = $this->get($key);
9✔
383
        }
384
        return $items;
9✔
385
    }
386

387
    /**
388
     * Set a session item.
389
     *
390
     * @param string $key The item key name
391
     * @param mixed $value The item value
392
     *
393
     * @rerun static
394
     */
395
    public function set(string $key, mixed $value) : static
396
    {
397
        $_SESSION[$key] = $value;
74✔
398
        return $this;
74✔
399
    }
400

401
    /**
402
     * Set multiple session items.
403
     *
404
     * @param array<string,mixed> $items An associative array of items keys and
405
     * values
406
     *
407
     * @rerun static
408
     */
409
    public function setMulti(array $items) : static
410
    {
411
        foreach ($items as $key => $value) {
9✔
412
            $this->set($key, $value);
9✔
413
        }
414
        return $this;
9✔
415
    }
416

417
    /**
418
     * Remove (unset) a session item.
419
     *
420
     * @param string $key The item key name
421
     *
422
     * @rerun static
423
     */
424
    public function remove(string $key) : static
425
    {
426
        unset($_SESSION[$key]);
18✔
427
        return $this;
18✔
428
    }
429

430
    /**
431
     * Remove (unset) multiple session items.
432
     *
433
     * @param array<string> $keys A list of items keys names
434
     *
435
     * @rerun static
436
     */
437
    public function removeMulti(array $keys) : static
438
    {
439
        foreach ($keys as $key) {
9✔
440
            $this->remove($key);
9✔
441
        }
442
        return $this;
9✔
443
    }
444

445
    /**
446
     * Remove (unset) all session items.
447
     *
448
     * @rerun static
449
     */
450
    public function removeAll() : static
451
    {
452
        @\session_unset();
9✔
453
        $_SESSION = [];
9✔
454
        return $this;
9✔
455
    }
456

457
    /**
458
     * Update the current session id with a newly generated one.
459
     *
460
     * @param bool $deleteOldSession Whether to delete the old associated session item or not
461
     *
462
     * @return bool
463
     */
464
    public function regenerateId(bool $deleteOldSession = false) : bool
465
    {
466
        $regenerated = \session_regenerate_id($deleteOldSession);
18✔
467
        if ($regenerated) {
18✔
468
            $_SESSION['$']['regenerated_at'] = \time();
18✔
469
        }
470
        return $regenerated;
18✔
471
    }
472

473
    /**
474
     * Re-initialize session array with original values.
475
     *
476
     * @return bool true if the session was successfully reinitialized or false on failure
477
     */
478
    public function reset() : bool
479
    {
480
        return \session_reset();
17✔
481
    }
482

483
    /**
484
     * Get a Flash Data item.
485
     *
486
     * @param string $key The Flash item key name
487
     *
488
     * @return mixed The item value or null if not exists
489
     */
490
    #[Pure]
491
    public function getFlash(string $key) : mixed
492
    {
493
        return $_SESSION['$']['flash']['new'][$key]
18✔
494
            ?? $_SESSION['$']['flash']['old'][$key]
18✔
495
            ?? null;
18✔
496
    }
497

498
    /**
499
     * Set a Flash Data item, available only in the next time the session is started.
500
     *
501
     * @param string $key The Flash Data item key name
502
     * @param mixed $value The item value
503
     *
504
     * @rerun static
505
     */
506
    public function setFlash(string $key, mixed $value) : static
507
    {
508
        $_SESSION['$']['flash']['new'][$key] = $value;
19✔
509
        return $this;
19✔
510
    }
511

512
    /**
513
     * Remove a Flash Data item.
514
     *
515
     * @param string $key The item key name
516
     *
517
     * @rerun static
518
     */
519
    public function removeFlash(string $key) : static
520
    {
521
        unset(
9✔
522
            $_SESSION['$']['flash']['old'][$key],
9✔
523
            $_SESSION['$']['flash']['new'][$key]
9✔
524
        );
9✔
525
        return $this;
9✔
526
    }
527

528
    /**
529
     * Get a Temp Data item.
530
     *
531
     * @param string $key The item key name
532
     *
533
     * @return mixed The item value or null if it is expired or not set
534
     */
535
    public function getTemp(string $key) : mixed
536
    {
537
        if (isset($_SESSION['$']['temp'][$key])) {
27✔
538
            if ($_SESSION['$']['temp'][$key]['ttl'] > \time()) {
27✔
539
                return $_SESSION['$']['temp'][$key]['data'];
27✔
540
            }
541
            unset($_SESSION['$']['temp'][$key]);
9✔
542
        }
543
        return null;
27✔
544
    }
545

546
    /**
547
     * Set a Temp Data item.
548
     *
549
     * @param string $key The item key name
550
     * @param mixed $value The item value
551
     * @param int $ttl The Time-To-Live of the item, in seconds
552
     *
553
     * @rerun static
554
     */
555
    public function setTemp(string $key, mixed $value, int $ttl = 60) : static
556
    {
557
        $_SESSION['$']['temp'][$key] = [
28✔
558
            'ttl' => \time() + $ttl,
28✔
559
            'data' => $value,
28✔
560
        ];
28✔
561
        return $this;
28✔
562
    }
563

564
    /**
565
     * Remove (unset) a Temp Data item.
566
     *
567
     * @param string $key The item key name
568
     *
569
     * @rerun static
570
     */
571
    public function removeTemp(string $key) : static
572
    {
573
        unset($_SESSION['$']['temp'][$key]);
9✔
574
        return $this;
9✔
575
    }
576

577
    /**
578
     * Get/Set the session id.
579
     *
580
     * @param string|null $newId [optional] The new session id
581
     *
582
     * @throws LogicException when trying to set a new id and the session is active
583
     *
584
     * @return false|string The old session id or false on failure. Note: If a
585
     * $newId is set, it is accepted but not validated. When session_start is
586
     * called, the id is only used if it is valid
587
     */
588
    public function id(?string $newId = null) : false | string
589
    {
590
        if ($newId !== null && $this->isActive()) {
29✔
591
            throw new LogicException(
9✔
592
                'Session ID cannot be changed when a session is active'
9✔
593
            );
9✔
594
        }
595
        return \session_id($newId);
29✔
596
    }
597

598
    /**
599
     * Perform session data garbage collection.
600
     *
601
     * If return false, use {@see error_get_last()} to get error details.
602
     *
603
     * @return false|int Returns the number of deleted session data for success,
604
     * false for failure
605
     */
606
    public function gc() : false | int
607
    {
608
        return @\session_gc();
2✔
609
    }
610

611
    public function setDebugCollector(SessionCollector $collector) : static
612
    {
613
        $this->debugCollector = $collector;
10✔
614
        $this->debugCollector->setSession($this)->setOptions($this->options);
10✔
615
        if (isset($this->saveHandler)) {
10✔
616
            $this->debugCollector->setSaveHandler($this->saveHandler);
5✔
617
        }
618
        return $this;
10✔
619
    }
620
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc