Skip to content

Commit 94d4fb2

Browse files
authored
PHPORM-310 Create dedicated session handler (#3348)
1 parent 251d6e2 commit 94d4fb2

File tree

5 files changed

+149
-13
lines changed

5 files changed

+149
-13
lines changed

Diff for: phpstan.neon.dist

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ parameters:
1111

1212
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
1313

14+
universalObjectCratesClasses:
15+
- MongoDB\BSON\Document
16+
1417
ignoreErrors:
1518
- '#Unsafe usage of new static#'
1619
- '#Call to an undefined method [a-zA-Z0-9\\_\<\>\(\)]+::[a-zA-Z]+\(\)#'

Diff for: src/MongoDBServiceProvider.php

+5-7
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
use MongoDB\Laravel\Eloquent\Model;
2424
use MongoDB\Laravel\Queue\MongoConnector;
2525
use MongoDB\Laravel\Scout\ScoutEngine;
26+
use MongoDB\Laravel\Session\MongoDbSessionHandler;
2627
use RuntimeException;
27-
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler;
2828

2929
use function assert;
3030
use function class_exists;
@@ -67,12 +67,10 @@ public function register()
6767
assert($connection instanceof Connection, new InvalidArgumentException(sprintf('The database connection "%s" used for the session does not use the "mongodb" driver.', $connectionName)));
6868

6969
return new MongoDbSessionHandler(
70-
$connection->getClient(),
71-
$app->config->get('session.options', []) + [
72-
'database' => $connection->getDatabaseName(),
73-
'collection' => $app->config->get('session.table') ?: 'sessions',
74-
'ttl' => $app->config->get('session.lifetime'),
75-
],
70+
$connection,
71+
$app->config->get('session.table', 'sessions'),
72+
$app->config->get('session.lifetime'),
73+
$app,
7674
);
7775
});
7876
});

Diff for: src/Session/MongoDbSessionHandler.php

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace MongoDB\Laravel\Session;
13+
14+
use Illuminate\Session\DatabaseSessionHandler;
15+
use MongoDB\BSON\Binary;
16+
use MongoDB\BSON\Document;
17+
use MongoDB\BSON\UTCDateTime;
18+
use MongoDB\Collection;
19+
20+
use function assert;
21+
use function tap;
22+
use function time;
23+
24+
/**
25+
* Session handler using the MongoDB driver extension.
26+
*/
27+
final class MongoDbSessionHandler extends DatabaseSessionHandler
28+
{
29+
private Collection $collection;
30+
31+
public function close(): bool
32+
{
33+
return true;
34+
}
35+
36+
public function gc($lifetime): int
37+
{
38+
$result = $this->getCollection()->deleteMany(['last_activity' => ['$lt' => $this->getUTCDateTime(-$lifetime)]]);
39+
40+
return $result->getDeletedCount() ?? 0;
41+
}
42+
43+
public function destroy($sessionId): bool
44+
{
45+
$this->getCollection()->deleteOne(['_id' => (string) $sessionId]);
46+
47+
return true;
48+
}
49+
50+
public function read($sessionId): string|false
51+
{
52+
$result = $this->getCollection()->findOne(
53+
['_id' => (string) $sessionId, 'expires_at' => ['$gte' => $this->getUTCDateTime()]],
54+
[
55+
'projection' => ['_id' => false, 'payload' => true],
56+
'typeMap' => ['root' => 'bson'],
57+
],
58+
);
59+
assert($result instanceof Document);
60+
61+
return $result ? (string) $result->payload : false;
62+
}
63+
64+
public function write($sessionId, $data): bool
65+
{
66+
$payload = $this->getDefaultPayload($data);
67+
68+
$this->getCollection()->replaceOne(
69+
['_id' => (string) $sessionId],
70+
$payload,
71+
['upsert' => true],
72+
);
73+
74+
return true;
75+
}
76+
77+
/** Creates a TTL index that automatically deletes expired objects. */
78+
public function createTTLIndex(): void
79+
{
80+
$this->collection->createIndex(
81+
// UTCDateTime field that holds the expiration date
82+
['expires_at' => 1],
83+
// Delay to remove items after expiration
84+
['expireAfterSeconds' => 0],
85+
);
86+
}
87+
88+
protected function getDefaultPayload($data): array
89+
{
90+
$payload = [
91+
'payload' => new Binary($data),
92+
'last_activity' => $this->getUTCDateTime(),
93+
'expires_at' => $this->getUTCDateTime($this->minutes * 60),
94+
];
95+
96+
if (! $this->container) {
97+
return $payload;
98+
}
99+
100+
return tap($payload, function (&$payload) {
101+
$this->addUserInformation($payload)
102+
->addRequestInformation($payload);
103+
});
104+
}
105+
106+
private function getCollection(): Collection
107+
{
108+
return $this->collection ??= $this->connection->getCollection($this->table);
109+
}
110+
111+
private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
112+
{
113+
return new UTCDateTime((time() + $additionalSeconds) * 1000);
114+
}
115+
}

Diff for: tests/Query/BuilderTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ function (Builder $builder) {
868868
[],
869869
],
870870
],
871-
fn (Builder $builder) => $builder->whereDate('created_at', '=', new DateTimeImmutable('2018-09-30 15:00:00 +02:00')),
871+
fn (Builder $builder) => $builder->whereDate('created_at', '=', new DateTimeImmutable('2018-09-30 15:00:00 +00:00')),
872872
];
873873

874874
yield 'where date !=' => [

Diff for: tests/SessionTest.php

+25-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
use Illuminate\Session\DatabaseSessionHandler;
66
use Illuminate\Session\SessionManager;
77
use Illuminate\Support\Facades\DB;
8-
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler;
8+
use MongoDB\Laravel\Session\MongoDbSessionHandler;
9+
use PHPUnit\Framework\Attributes\TestWith;
10+
use SessionHandlerInterface;
911

1012
class SessionTest extends TestCase
1113
{
@@ -16,21 +18,31 @@ protected function tearDown(): void
1618
parent::tearDown();
1719
}
1820

19-
public function testDatabaseSessionHandlerCompatibility()
21+
/** @param class-string<SessionHandlerInterface> $class */
22+
#[TestWith([DatabaseSessionHandler::class])]
23+
#[TestWith([MongoDbSessionHandler::class])]
24+
public function testSessionHandlerFunctionality(string $class)
2025
{
21-
$sessionId = '123';
22-
23-
$handler = new DatabaseSessionHandler(
26+
$handler = new $class(
2427
$this->app['db']->connection('mongodb'),
2528
'sessions',
2629
10,
2730
);
2831

32+
$sessionId = '123';
33+
2934
$handler->write($sessionId, 'foo');
3035
$this->assertEquals('foo', $handler->read($sessionId));
3136

3237
$handler->write($sessionId, 'bar');
3338
$this->assertEquals('bar', $handler->read($sessionId));
39+
40+
$handler->destroy($sessionId);
41+
$this->assertEmpty($handler->read($sessionId));
42+
43+
$handler->write($sessionId, 'bar');
44+
$handler->gc(-1);
45+
$this->assertEmpty($handler->read($sessionId));
3446
}
3547

3648
public function testDatabaseSessionHandlerRegistration()
@@ -70,5 +82,13 @@ private function assertSessionCanStoreInMongoDB(SessionManager $session): void
7082

7183
self::assertIsObject($data);
7284
self::assertSame($session->getId(), $data->_id);
85+
86+
$session->remove('foo');
87+
$data = DB::connection('mongodb')
88+
->getCollection('sessions')
89+
->findOne(['_id' => $session->getId()]);
90+
91+
self::assertIsObject($data);
92+
self::assertSame($session->getId(), $data->_id);
7393
}
7494
}

0 commit comments

Comments
 (0)