8000 feature #45873 [HttpFoundation] Allow dynamic session "ttl" when usin… · symfony/symfony@acee03f · GitHub
[go: up one dir, main page]

Skip to content

Commit acee03f

Browse files
committed
feature #45873 [HttpFoundation] Allow dynamic session "ttl" when using a remote storage (nicolas-grekas)
This PR was merged into the 6.1 branch. Discussion ---------- [HttpFoundation] Allow dynamic session "ttl" when using a remote storage | Q | A | ------------- | --- | Branch? | 6.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #44368 | License | MIT | Doc PR | - This is my proposal instead of #44368. My goal here it to *not* make the session handler mutable. Instead, I propose to allow defining the "ttl" option as a closure. Here is how this would work in practice: ```yaml services: my.session.handler: parent: session.abstract_handler arguments: index_0: 'redis://localhost' index_1: {ttl: !closure ['@my.ttl.handler']} ``` Where `my.ttl.handler` would be an invokable that returns the ttl for the current user (based on the session object from the request stack, or from the token storage I suppose). /cc @Seldaek I'd be happy to have your thoughts here (and also your help for tests/doc ideally 👼) Commits ------- 51fc454 [HttpFoundation] Allow dynamic session "ttl" when using a remote storage
2 parents 7cdf8b4 + 51fc454 commit acee03f

File tree

11 files changed

+74
-18
lines changed

11 files changed

+74
-18
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/session.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777

7878
->set('session.abstract_handler', AbstractSessionHandler::class)
7979
->factory([SessionHandlerFactory::class, 'createHandler'])
80-
->args([abstract_arg('A string or a connection object')])
80+
->args([abstract_arg('A string or a connection object'), []])
8181

8282
->set('session_listener', SessionListener::class)
8383
->args([

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add stale while revalidate and stale if error cache header
8+
* Allow dynamic session "ttl" when using a remote storage
89

910
6.0
1011
---

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class MemcachedSessionHandler extends AbstractSessionHandler
2626
/**
2727
* Time to live in seconds.
2828
*/
29-
private ?int $ttl;
29+
private int|\Closure|null $ttl;
3030

3131
/**
3232
* Key prefix for shared environments.
@@ -69,7 +69,8 @@ protected function doRead(string $sessionId): string
6969

7070
public function updateTimestamp(string $sessionId, string $data): bool
7171
{
72-
$this->memcached->touch($this->prefix.$sessionId, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
72+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
73+
$this->memcached->touch($this->prefix.$sessionId, time() + (int) $ttl);
7374

7475
return true;
7576
}
@@ -79,7 +80,9 @@ public function updateTimestamp(string $sessionId, string $data): bool
7980
*/
8081
protected function doWrite(string $sessionId, string $data): bool
8182
{
82-
return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
83+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
84+
85+
return $this->memcached->set($this->prefix.$sessionId, $data, time() + (int) $ttl);
8386
}
8487

8588
/**

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MongoDbSessionHandler.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class MongoDbSessionHandler extends AbstractSessionHandler
2929
private Client $mongo;
3030
private Collection $collection;
3131
private array $options;
32+
private int|\Closure|null $ttl;
3233

3334
/**
3435
* Constructor.
@@ -39,7 +40,8 @@ class MongoDbSessionHandler extends AbstractSessionHandler
3940
* * id_field: The field name for storing the session id [default: _id]
4041
* * data_field: The field name for storing the session data [default: data]
4142
* * time_field: The field name for storing the timestamp [default: time]
42-
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at].
43+
* * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]
44+
* * ttl: The time to live in seconds.
4345
*
4446
* It is strongly recommended to put an index on the `expiry_field` for
4547
* garbage-collection. Alternatively it's possible to automatically expire
@@ -74,6 +76,7 @@ public function __construct(Client $mongo, array $options)
7476
'time_field' => 'time',
7577
'expiry_field' => 'expires_at',
7678
], $options);
79+
$this->ttl = $this->options['ttl'] ?? null;
7780
}
7881

7982
public function close(): bool
@@ -105,7 +108,8 @@ public function gc(int $maxlifetime): int|false
105108
*/
106109
protected function doWrite(string $sessionId, string $data): bool
107110
{
108-
$expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
111+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
112+
$expiry = new UTCDateTime((time() + (int) $ttl) * 1000);
109113

110114
$fields = [
111115
$this->options['time_field'] => new UTCDateTime(),
@@ -124,7 +128,8 @@ protected function doWrite(string $sessionId, string $data): bool
124128

125129
public function updateTimestamp(string $sessionId, string $data): bool
126130
{
127-
$expiry = new UTCDateTime((time() + (int) ini_get('session.gc_maxlifetime')) * 1000);
131+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
132+
$expiry = new UTCDateTime((time() + (int) $ttl) * 1000);
128133

129134
$this->getCollection()->updateOne(
130135
[$this->options['id_field'] => $sessionId],

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ class PdoSessionHandler extends AbstractSessionHandler
7979
private string $lifetimeCol = 'sess_lifetime';
8080
private string $timeCol = 'sess_time';
8181

82+
/**
83+
* Time to live in seconds.
84+
*/
85+
private int|\Closure|null $ttl;
86+
8287
/**
8388
* Username when lazy-connect.
8489
*/
@@ -137,6 +142,7 @@ class PdoSessionHandler extends AbstractSessionHandler
137142
* * db_password: The password when lazy-connect [default: '']
138143
* * db_connection_options: An array of driver-specific connection options [default: []]
139144
* * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
145+
* * ttl: The time to live in seconds.
140146
*
141147
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null
142148
*
@@ -166,6 +172,7 @@ public function __construct(\PDO|string $pdoOrDsn = null, array $options = [])
166172
$this->password = $options['db_password'] ?? $this->password;
167173
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
168174
$this->lockMode = $options['lock_mode'] ?? $this->lockMode;
175+
$this->ttl = $options['ttl'] ?? null;
169176
}
170177

171178
/**
@@ -275,7 +282,7 @@ protected function doDestroy(string $sessionId): bool
275282
*/
276283
protected function doWrite(string $sessionId, string $data): bool
277284
{
278-
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
285+
$maxlifetime = (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime'));
279286

280287
try {
281288
// We use a single MERGE SQL query when supported by the database.
@@ -318,7 +325,7 @@ protected function doWrite(string $sessionId, string $data): bool
318325

319326
public function updateTimestamp(string $sessionId, string $data): bool
320327
{
321-
$expiry = time() + (int) ini_get('session.gc_maxlifetime');
328+
$expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime'));
322329

323330
try {
324331
$updateStmt = $this->pdo->prepare(

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class RedisSessionHandler extends AbstractSessionHandler
3333
/**
3434
* Time to live in seconds.
3535
*/
36-
private ?int $ttl;
36+
private int|\Closure|null $ttl;
3737

3838
/**
3939
* List of available options:
@@ -66,7 +66,8 @@ protected function doRead(string $sessionId): string
6666
*/
6767
protected function doWrite(string $sessionId, string $data): bool
6868
{
69-
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')), $data);
69+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
70+
$result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data);
7071

7172
return $result && !$result instanceof ErrorInterface;
7273
}
@@ -109,6 +110,8 @@ public function gc(int $maxlifetime): int|false
109110

110111
public function updateTimestamp(string $sessionId, string $data): bool
111112
{
112-
return $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? ini_get('session.gc_maxlifetime')));
113+
$ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? ini_get('session.gc_maxlifetime');
114+
115+
return $this->redis->expire($this->prefix.$sessionId, (int) $ttl);
113116
}
114117
}

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@
2121
*/
2222
class SessionHandlerFactory
2323
{
24-
public static function createHandler(object|string $connection): AbstractSessionHandler
24+
public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler
2525
{
26-
if ($options = \is_string($connection) ? parse_url($connection) : false) {
27-
parse_str($options['query'] ?? '', $options);
26+
if ($query = \is_string($connection) ? parse_url($connection) : false) {
27+
parse_str($query['query'] ?? '', $query);
28+
29+
if (($options['ttl'] ?? null) instanceof \Closure) {
30+
$query['ttl'] = $options['ttl'];
31+
}
2832
}
33+
$options = ($query ?: []) + $options;
2934

3035
switch (true) {
3136
case $connection instanceof \Redis:
@@ -58,7 +63,7 @@ public static function createHandler(object|string $connection): AbstractSession
5863
$handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
5964
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
6065

61-
return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1]));
66+
return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1]));
6267

6368
case str_starts_with($connection, 'pdo_oci://'):
6469
if (!class_exists(DriverManager::class)) {
@@ -76,7 +81,7 @@ public static function createHandler(object|string $connection): AbstractSession
7681
case str_starts_with($connection, 'sqlsrv://'):
7782
case str_starts_with($connection, 'sqlite://'):
7883
case str_starts_with($connection, 'sqlite3://'):
79-
return new PdoSessionHandler($connection, $options ?: []);
84+
return new PdoSessionHandler($connection, $options);
8085
}
8186

8287
throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractRedisSessionHandlerTestCase.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,19 @@ public function testUseTtlOption(int $ttl)
164164

10000
165165
$this->assertLessThan($redisTtl, $ttl - 5);
166166
$this->assertGreaterThan($redisTtl, $ttl + 5);
167+
168+
$options = [
169+
'prefix' => self::PREFIX,
170+
'ttl' => fn () => $ttl,
171+
];
172+
173+
$handler = new RedisSessionHandler($this->redisClient, $options);
174+
$handler->write('id', 'data');
175+
$redisTtl = $this->redisClient->ttl(self::PREFIX.'id');
176+
177+
$this->assertLessThan($redisTtl, $ttl - 5);
178+
$this->assertGreaterThan($redisTtl, $ttl + 5);
179+
167180
}
168181

169182
public function getTtlFixtures(): array

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public function getOptionFixtures()
134134
return [
135135
[['prefix' => 'session'], true],
136136
[['expiretime' => 100], true],
137-
[['prefix' => 'session', 'expiretime' => 200], true],
137+
[['prefix' => 'session', 'ttl' => 200], true],
138138
[['expiretime' => 100, 'foo' => 'bar'], false],
139139
];
140140
}

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,21 @@ public function provideUrlDsnPairs()
345345
yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
346346
}
347347

348+
public function testTtl()
349+
{
350+
foreach ([60, fn () => 60] as $ttl) {
351+
$pdo = $this->getMemorySqlitePdo();
352+
$storage = new PdoSessionHandler($pdo, ['ttl' => $ttl]);
353+
354+
$storage->open('', 'sid');
355+
$storage->read('id');
356+
$storage->write('id', 'data');
357+
$storage->close();
358+
359+
$this->assertEqualsWithDelta(time() + 60, $pdo->query('SELECT sess_lifetime FROM sessions')->fetchColumn(), 5);
360+
}
361+
}
362+
348363
/**
349364
* @return resource
350365
*/

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,9 @@ public function testCreateRedisHandlerFromDsn()
7171

7272
$ttlProperty = $reflection->getProperty('ttl');
7373
$this->assertSame(3600, $ttlProperty->getValue($handler));
74+
75+
$handler = SessionHandlerFactory::createHandler('redis://localhost?prefix=foo&ttl=3600&ignored=bar', ['ttl' => function () { return 123; }]);
76+
77+
$this->assertInstanceOf(\Closure::class, $reflection->getProperty('ttl')->getValue($handler));
7478
}
7579
}

0 commit comments

Comments
 (0)
0