1: <?php
2:
3: /*
4: * This file is part of the Symfony package.
5: *
6: * (c) Fabien Potencier <fabien@symfony.com>
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 Symfony\Component\HttpFoundation\Session\Storage\Handler;
13:
14: /**
15: * MongoDB session handler.
16: *
17: * @author Markus Bachmann <markus.bachmann@bachi.biz>
18: */
19: class MongoDbSessionHandler implements \SessionHandlerInterface
20: {
21: /**
22: * @var \Mongo
23: */
24: private $mongo;
25:
26: /**
27: * @var \MongoCollection
28: */
29: private $collection;
30:
31: /**
32: * @var array
33: */
34: private $options;
35:
36: /**
37: * Constructor.
38: *
39: * List of available options:
40: * * database: The name of the database [required]
41: * * collection: The name of the collection [required]
42: * * id_field: The field name for storing the session id [default: _id]
43: * * data_field: The field name for storing the session data [default: data]
44: * * time_field: The field name for storing the timestamp [default: time]
45: *
46: * @param \Mongo|\MongoClient $mongo A MongoClient or Mongo instance
47: * @param array $options An associative array of field options
48: *
49: * @throws \InvalidArgumentException When MongoClient or Mongo instance not provided
50: * @throws \InvalidArgumentException When "database" or "collection" not provided
51: */
52: public function __construct($mongo, array $options)
53: {
54: if (!($mongo instanceof \MongoClient || $mongo instanceof \Mongo)) {
55: throw new \InvalidArgumentException('MongoClient or Mongo instance required');
56: }
57:
58: if (!isset($options['database']) || !isset($options['collection'])) {
59: throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler');
60: }
61:
62: $this->mongo = $mongo;
63:
64: $this->options = array_merge(array(
65: 'id_field' => '_id',
66: 'data_field' => 'data',
67: 'time_field' => 'time',
68: 'expiry_field' => false,
69: ), $options);
70: }
71:
72: /**
73: * {@inheritdoc}
74: */
75: public function open($savePath, $sessionName)
76: {
77: return true;
78: }
79:
80: /**
81: * {@inheritdoc}
82: */
83: public function close()
84: {
85: return true;
86: }
87:
88: /**
89: * {@inheritdoc}
90: */
91: public function destroy($sessionId)
92: {
93: $this->getCollection()->remove(array(
94: $this->options['id_field'] => $sessionId,
95: ));
96:
97: return true;
98: }
99:
100: /**
101: * {@inheritdoc}
102: */
103: public function gc($maxlifetime)
104: {
105: /* Note: MongoDB 2.2+ supports TTL collections, which may be used in
106: * place of this method by indexing the "time_field" field with an
107: * "expireAfterSeconds" option. Regardless of whether TTL collections
108: * are used, consider indexing this field to make the remove query more
109: * efficient.
110: *
111: * See: http://docs.mongodb.org/manual/tutorial/expire-data/
112: */
113: if (false !== $this->options['expiry_field']) {
114: return true;
115: }
116: $time = new \MongoDate(time() - $maxlifetime);
117:
118: $this->getCollection()->remove(array(
119: $this->options['time_field'] => array('$lt' => $time),
120: ));
121:
122: return true;
123: }
124:
125: /**
126: * {@inheritdoc}
127: */
128: public function write($sessionId, $data)
129: {
130: $fields = array(
131: $this->options['data_field'] => new \MongoBinData($data, \MongoBinData::BYTE_ARRAY),
132: $this->options['time_field'] => new \MongoDate(),
133: );
134:
135: /* Note: As discussed in the gc method of this class. You can utilise
136: * TTL collections in MongoDB 2.2+
137: * We are setting the "expiry_field" as part of the write operation here
138: * You will need to create the index on your collection that expires documents
139: * at that time
140: * e.g.
141: * db.MySessionCollection.ensureIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } )
142: */
143: if (false !== $this->options['expiry_field']) {
144: $expiry = new \MongoDate(time() + (int) ini_get('session.gc_maxlifetime'));
145: $fields[$this->options['expiry_field']] = $expiry;
146: }
147:
148: $this->getCollection()->update(
149: array($this->options['id_field'] => $sessionId),
150: array('$set' => $fields),
151: array('upsert' => true, 'multiple' => false)
152: );
153:
154: return true;
155: }
156:
157: /**
158: * {@inheritdoc}
159: */
160: public function read($sessionId)
161: {
162: $dbData = $this->getCollection()->findOne(array(
163: $this->options['id_field'] => $sessionId,
164: ));
165:
166: return null === $dbData ? '' : $dbData[$this->options['data_field']]->bin;
167: }
168:
169: /**
170: * Return a "MongoCollection" instance.
171: *
172: * @return \MongoCollection
173: */
174: private function getCollection()
175: {
176: if (null === $this->collection) {
177: $this->collection = $this->mongo->selectCollection($this->options['database'], $this->options['collection']);
178: }
179:
180: return $this->collection;
181: }
182:
183: /**
184: * Return a Mongo instance
185: *
186: * @return \Mongo
187: */
188: protected function getMongo()
189: {
190: return $this->mongo;
191: }
192: }
193: