Skip to content

Commit b9a3528

Browse files
authored
Merge pull request #421 from bowphp/refactor/code-base
fix(session): fix database adaptor
2 parents a295a5f + d16d70e commit b9a3528

2 files changed

Lines changed: 119 additions & 15 deletions

File tree

src/Session/Adapters/DatabaseAdapter.php

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ class DatabaseAdapter implements SessionHandlerInterface
2020
*/
2121
private string $table;
2222

23-
/**
24-
* The current session session_id
25-
*
26-
* @var string
27-
*/
28-
private string $session_id;
29-
3023
/**
3124
* The current user ip
3225
*
@@ -91,11 +84,11 @@ private function sessions(): QueryBuilder
9184
*/
9285
public function gc(int $max_lifetime): int|false
9386
{
94-
$this->sessions()
95-
->where('time', '<', $this->createTimestamp())
87+
// The `time` column stores each session's expiry timestamp, so a
88+
// session is collectable once that expiry is in the past.
89+
return $this->sessions()
90+
->where('time', '<', date('Y-m-d H:i:s'))
9691
->delete();
97-
98-
return 1;
9992
}
10093

10194
/**
@@ -117,10 +110,14 @@ public function open(string $path, string $name): bool
117110
* @return string
118111
* @throws QueryBuilderException
119112
*/
120-
public function read(string $id): string
113+
public function read(string $session_id): string
121114
{
115+
// Only return live sessions: an expired row (expiry in the past) must
116+
// be treated as absent, otherwise stale sessions stay usable until gc.
122117
$session = $this->sessions()
123-
->where('id', $id)->first();
118+
->where('id', $session_id)
119+
->where('time', '>=', date('Y-m-d H:i:s'))
120+
->first();
124121

125122
if (is_null($session)) {
126123
return '';
@@ -147,11 +144,12 @@ public function write(string $id, string $data): bool
147144
return (bool)$insert;
148145
}
149146

150-
// Update the session information
147+
// Update the session payload and slide the expiry forward so an active
148+
// session does not expire a fixed window after its first request.
151149
$update = $this->sessions()->where('id', $id)->update(
152150
[
153151
'data' => $data,
154-
'id' => $id
152+
'time' => $this->createTimestamp()
155153
]
156154
);
157155

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace Bow\Tests\Session;
4+
5+
use Bow\Database\Database;
6+
use Bow\Session\Adapters\DatabaseAdapter;
7+
use Bow\Tests\Config\TestingConfiguration;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DatabaseAdapterTest extends TestCase
11+
{
12+
private DatabaseAdapter $adapter;
13+
14+
public static function setUpBeforeClass(): void
15+
{
16+
$config = TestingConfiguration::getConfig();
17+
18+
Database::configure($config["database"]);
19+
Database::connection('sqlite');
20+
Database::statement("DROP TABLE IF EXISTS sessions;");
21+
Database::statement("
22+
CREATE TABLE sessions (
23+
id varchar(200) not null primary key,
24+
time datetime null,
25+
data text null,
26+
ip varchar(255) null
27+
)");
28+
}
29+
30+
protected function setUp(): void
31+
{
32+
Database::connection('sqlite');
33+
Database::statement("DELETE FROM sessions;");
34+
35+
$this->adapter = new DatabaseAdapter(['table' => 'sessions'], '127.0.0.1');
36+
}
37+
38+
/**
39+
* Seed a row directly with an explicit expiry offset so each test controls
40+
* exactly whether a session is expired or still active.
41+
*/
42+
private function seed(string $id, string $data, int $expiresInSeconds): void
43+
{
44+
Database::table('sessions')->insert([
45+
'id' => $id,
46+
'time' => date('Y-m-d H:i:s', time() + $expiresInSeconds),
47+
'data' => $data,
48+
'ip' => '127.0.0.1',
49+
]);
50+
}
51+
52+
private function exists(string $id): bool
53+
{
54+
return Database::table('sessions')->where('id', $id)->exists();
55+
}
56+
57+
/** Bug #1 — gc() must drop expired rows but keep still-valid ones. */
58+
public function test_gc_removes_only_expired_sessions(): void
59+
{
60+
$this->seed('expired-session', 'old', -60); // expired a minute ago
61+
$this->seed('active-session', 'fresh', 3600); // valid for another hour
62+
63+
$this->adapter->gc(0);
64+
65+
$this->assertFalse($this->exists('expired-session'), 'Expired session should be collected');
66+
$this->assertTrue($this->exists('active-session'), 'Active session must survive gc');
67+
}
68+
69+
/** Bug #3 — read() must not return data for an expired session. */
70+
public function test_read_returns_empty_for_expired_session(): void
71+
{
72+
$this->seed('stale', 'secret-payload', -60);
73+
74+
$this->assertSame('', $this->adapter->read('stale'));
75+
}
76+
77+
/** read() still returns the payload for a live session. */
78+
public function test_read_returns_data_for_active_session(): void
79+
{
80+
$this->seed('live', 'hello world', 3600);
81+
82+
$this->assertSame('hello world', $this->adapter->read('live'));
83+
}
84+
85+
/** Bug #2 — write() on an existing session must refresh its expiry (sliding window). */
86+
public function test_write_refreshes_expiry_on_update(): void
87+
{
88+
$this->adapter->write('rolling', 'v1');
89+
90+
// Simulate time passing: push the stored expiry into the past.
91+
Database::table('sessions')
92+
->where('id', 'rolling')
93+
->update(['time' => date('Y-m-d H:i:s', time() - 60)]);
94+
95+
$this->adapter->write('rolling', 'v2');
96+
97+
$row = Database::table('sessions')->where('id', 'rolling')->first();
98+
99+
$this->assertSame('v2', $row->data, 'Payload should be updated');
100+
$this->assertGreaterThan(
101+
date('Y-m-d H:i:s'),
102+
$row->time,
103+
'Expiry must be pushed back into the future on each write'
104+
);
105+
}
106+
}

0 commit comments

Comments
 (0)