MongoDbSessionHandler.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.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 Symfony\Component\HttpFoundation\Session\Storage\Handler;
  11. use MongoDB\BSON\Binary;
  12. use MongoDB\BSON\UTCDateTime;
  13. use MongoDB\Client;
  14. use MongoDB\Driver\BulkWrite;
  15. use MongoDB\Driver\Manager;
  16. use MongoDB\Driver\Query;
  17. /**
  18. * Session handler using the MongoDB driver extension.
  19. *
  20. * @author Markus Bachmann <markus.bachmann@bachi.biz>
  21. * @author Jérôme Tamarelle <jerome@tamarelle.net>
  22. *
  23. * @see https://php.net/mongodb
  24. */
  25. class MongoDbSessionHandler extends AbstractSessionHandler
  26. {
  27. private Manager $manager;
  28. private string $namespace;
  29. private array $options;
  30. private int|\Closure|null $ttl;
  31. /**
  32. * Constructor.
  33. *
  34. * List of available options:
  35. * * database: The name of the database [required]
  36. * * collection: The name of the collection [required]
  37. * * id_field: The field name for storing the session id [default: _id]
  38. * * data_field: The field name for storing the session data [default: data]
  39. * * time_field: The field name for storing the timestamp [default: time]
  40. * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]
  41. * * ttl: The time to live in seconds.
  42. *
  43. * It is strongly recommended to put an index on the `expiry_field` for
  44. * garbage-collection. Alternatively it's possible to automatically expire
  45. * the sessions in the database as described below:
  46. *
  47. * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions
  48. * automatically. Such an index can for example look like this:
  49. *
  50. * db.<session-collection>.createIndex(
  51. * { "<expiry-field>": 1 },
  52. * { "expireAfterSeconds": 0 }
  53. * )
  54. *
  55. * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/
  56. *
  57. * If you use such an index, you can drop `gc_probability` to 0 since
  58. * no garbage-collection is required.
  59. *
  60. * @throws \InvalidArgumentException When "database" or "collection" not provided
  61. */
  62. public function __construct(Client|Manager $mongo, array $options)
  63. {
  64. if (!isset($options['database']) || !isset($options['collection'])) {
  65. throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.');
  66. }
  67. if ($mongo instanceof Client) {
  68. $mongo = $mongo->getManager();
  69. }
  70. $this->manager = $mongo;
  71. $this->namespace = $options['database'].'.'.$options['collection'];
  72. $this->options = array_merge([
  73. 'id_field' => '_id',
  74. 'data_field' => 'data',
  75. 'time_field' => 'time',
  76. 'expiry_field' => 'expires_at',
  77. ], $options);
  78. $this->ttl = $this->options['ttl'] ?? null;
  79. }
  80. public function close(): bool
  81. {
  82. return true;
  83. }
  84. protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool
  85. {
  86. $write = new BulkWrite();
  87. $write->delete(
  88. [$this->options['id_field'] => $sessionId],
  89. ['limit' => 1]
  90. );
  91. $this->manager->executeBulkWrite($this->namespace, $write);
  92. return true;
  93. }
  94. public function gc(int $maxlifetime): int|false
  95. {
  96. $write = new BulkWrite();
  97. $write->delete(
  98. [$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]],
  99. );
  100. $result = $this->manager->executeBulkWrite($this->namespace, $write);
  101. return $result->getDeletedCount() ?? false;
  102. }
  103. protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool
  104. {
  105. $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
  106. $expiry = $this->getUTCDateTime($ttl);
  107. $fields = [
  108. $this->options['time_field'] => $this->getUTCDateTime(),
  109. $this->options['expiry_field'] => $expiry,
  110. $this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC),
  111. ];
  112. $write = new BulkWrite();
  113. $write->update(
  114. [$this->options['id_field'] => $sessionId],
  115. ['$set' => $fields],
  116. ['upsert' => true]
  117. );
  118. $this->manager->executeBulkWrite($this->namespace, $write);
  119. return true;
  120. }
  121. public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool
  122. {
  123. $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime');
  124. $expiry = $this->getUTCDateTime($ttl);
  125. $write = new BulkWrite();
  126. $write->update(
  127. [$this->options['id_field'] => $sessionId],
  128. ['$set' => [
  129. $this->options['time_field'] => $this->getUTCDateTime(),
  130. $this->options['expiry_field'] => $expiry,
  131. ]],
  132. ['multi' => false],
  133. );
  134. $this->manager->executeBulkWrite($this->namespace, $write);
  135. return true;
  136. }
  137. protected function doRead(#[\SensitiveParameter] string $sessionId): string
  138. {
  139. $cursor = $this->manager->executeQuery($this->namespace, new Query([
  140. $this->options['id_field'] => $sessionId,
  141. $this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()],
  142. ], [
  143. 'projection' => [
  144. '_id' => false,
  145. $this->options['data_field'] => true,
  146. ],
  147. 'limit' => 1,
  148. ]));
  149. foreach ($cursor as $document) {
  150. return (string) $document->{$this->options['data_field']} ?? '';
  151. }
  152. // Not found
  153. return '';
  154. }
  155. private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime
  156. {
  157. return new UTCDateTime((time() + $additionalSeconds) * 1000);
  158. }
  159. }