├── .gitignore ├── .php_cs ├── composer.json ├── LICENSE ├── README.md ├── composer.lock └── MongoSession.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | vendor/ 3 | 4 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | notName('LICENSE') 7 | ->notName('README.md') 8 | ->notName('.php_cs') 9 | ->notName('composer.*') 10 | ->notName('phpunit.xml*') 11 | ->notName('*.phar') 12 | ->exclude('vendor') 13 | ->exclude('Symfony/CS/Tests/Fixer') 14 | ->notName('ElseifFixer.php') 15 | ->exclude('data') 16 | ->in(__DIR__) 17 | ; 18 | 19 | return Symfony\CS\Config\Config::create() 20 | ->finder($finder) 21 | ; 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nicktacular/php-mongo-session", 3 | "type": "library", 4 | "homepage": "http://github.com/nicktacular/php-mongo-session", 5 | "time": "2014-11-28 19:00:01", 6 | "license": "MIT", 7 | "description": "PHP session handler for MongoDB.", 8 | "authors": [ 9 | { 10 | "name": "Nick Ilyin", 11 | "email": "nick.ilyin@gmail.com", 12 | "homepage": "http://github.com/nicktacular", 13 | "role": "Developer" 14 | } 15 | ], 16 | "repositories": [ 17 | { 18 | "type": "vcs", 19 | "url": "https://github.com/nicktacular/php-mongo-session" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.2.17" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": ">=3.7.22" 27 | }, 28 | "autoload": { 29 | "files": [ 30 | "MongoSession.php" 31 | ] 32 | }, 33 | "version": "1.0.1" 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Nicholas Ilyin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-mongo-session 2 | ================= 3 | 4 | A PHP session handler with a Mongo DB backend. 5 | 6 | Requirements 7 | ============ 8 | 9 | You'll need: 10 | * PHP version 5.2.17+ 11 | * PHP MongoDB driver version 1.3+. 12 | * A sense of adventure. 13 | 14 | Quickstart 15 | ========== 16 | 17 | Pretty simple, actually. First, include `MongoSession` class like so: 18 | 19 | ```php 20 | require_once 'MongoSession.php'; 21 | ``` 22 | 23 | Then you need to configure it. The most basic config looks like this: 24 | 25 | ```php 26 | MongoSession::config(array( 27 | 'connection' => 'mongodb://localhost:27017', 28 | 'db' => 'theDbName', 29 | 'cookie_domain' => '.mydomain.com' 30 | )); 31 | ``` 32 | 33 | Replace `.mydomain.com` with your domain and change the connection string to any 34 | valid MongoDB connection string as [described here](http://www.php.net/manual/en/mongo.connecting.php). 35 | 36 | Now, when you're ready, just call: 37 | 38 | ```php 39 | MongoSession::init(); 40 | ``` 41 | 42 | You can pass `true` to `init(…)` the first time it runs so that you create the indices, but that's it. 43 | 44 | You can pass options to MongoClient() via `connection_opts` in config. 45 | 46 | Ex, connect to replicaSet and define timeout: 47 | 48 | ```php 49 | MongoSession::config(array( 50 | 'connection' => 'mongodb://mongo1:27017,mongo2:27017', 51 | 'connection_opts' => array( 52 | 'replicaSet' =>'rs1', 53 | 'connecttimeoutMS' =>5000, 54 | ), 55 | 'cookie_domain' => '.mydomain.com', 56 | 'db' => 'theDbName' 57 | ); 58 | ``` 59 | 60 | Sessions are hard 61 | ================= 62 | 63 | Sessions are something we take for granted in PHP since they just work straight out of the box. You call 64 | `session_start()` and things just work well and quickly. Done. 65 | 66 | Ok, now that you're done developing on a single-machine architecture, let's briefly discuss multi-machine 67 | environments. You can no longer rely on the tried-and-true file-based session management from PHP. Time 68 | to use a centralized system. 69 | 70 | So why not an existing session handler built on MySQL? Well, 95% of the features in MySQL aren't necessary 71 | for session handling. MongoDB gives you exactly what you need and at a significant performance boost. Less 72 | overhead is good, right? 73 | 74 | The most important point I'd like to make after having dealt with a lot of pain of multi-server architectures 75 | is that there are many variables that affect how your application will now work. 76 | 77 | ### File locks 78 | 79 | PHP file-based sessions rely on file locks which are inherently released after a process completes, 80 | regardless of whether or not the process crashed, `exit`ed, or ran out of memory. This makes file-based 81 | sessions very reliable. 82 | 83 | Unlike file-based sessions, using a data store like MongoDB requires the application to deal with the 84 | locks. If your script runs out of memory and PHP crashes without the application releasing the lock, 85 | you could end up with a session that's completely locked out to the user. I've been able to partially 86 | replicate this problem on AWS EC2 Micro instances with a memory hungry app. Occasional crashes would 87 | occur under heavy load causing PHP not to finish the session with `write()` and `close()` which would 88 | invoking the unlock mechanism. 89 | 90 | You can't test this type of behavior in a unit test. You must test your entire application under load 91 | to ensure that you're running sufficient resources so that your application is running stable. 92 | 93 | ### MongoDB 94 | 95 | If you're going to try running session handling on a single MongoDB instance, you're most likely 96 | asking for trouble. Each application obviously has its own requirements and load so you need to 97 | analyze this. However, running a replica set with 3 or more machines is a good place to start. 98 | 99 | ### Acknowledges 100 | 101 | By default this class will require acknowledged writes from the primary only. You can configure the 102 | write concern as desired using the 'write_concern' configuration option. For example, set 'write_concern' 103 | to 2 to require acknowledgment from your primary and one secondary. 104 | 105 | You must not pass an integer 'write_concern' value as a string or it will be interpreted as a replica tag set. 106 | 107 | ### Journaling 108 | 109 | By default this class does not require journaling before acknowleding the write. You can override this 110 | behavior and require writes are journaled before ack'd by setting the 'write_journal' boolean. When set 111 | to true, Mongo will flush the write to disk from memory before acknowledging the write. 112 | 113 | Why, oh why? 114 | ============ 115 | 116 | If you've examined the code, I'm sure you have a few questions. 117 | 118 | **(Q) Why is this PHP 5.2 compatible? Nothing cool in PHP ever existed prior to 5.3** 119 | 120 | (A) You're preaching to the choir, but it's a legacy app thing. I'll make it 5.4 flavored eventually. 121 | 122 | **(Q) WHERE are your unit tests?** 123 | 124 | (This is a question [@grmpyprogrammer](https://twitter.com/grmpyprogrammer) is surely asking right about now…) 125 | 126 | (A) I'm working on it, but PHP sessions are inherently difficult to test. You can't really mock PHP calling your 127 | session handler so I'm working on figuring out a test. Regardless, I'm using this in a production environment 128 | so I as find optimizations or bugs, I'll be sure to post these here in addition to the unit test I'm trying to 129 | wrap my head around. 130 | 131 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" 5 | ], 6 | "hash": "1dce9534040145c3b7dcf635e47ab6b3", 7 | "packages": [ 8 | 9 | ], 10 | "packages-dev": [ 11 | { 12 | "name": "phpunit/php-code-coverage", 13 | "version": "1.2.12", 14 | "source": { 15 | "type": "git", 16 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 17 | "reference": "1.2.12" 18 | }, 19 | "dist": { 20 | "type": "zip", 21 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.12", 22 | "reference": "1.2.12", 23 | "shasum": "" 24 | }, 25 | "require": { 26 | "php": ">=5.3.3", 27 | "phpunit/php-file-iterator": ">=1.3.0@stable", 28 | "phpunit/php-text-template": ">=1.1.1@stable", 29 | "phpunit/php-token-stream": ">=1.1.3@stable" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "3.7.*@dev" 33 | }, 34 | "suggest": { 35 | "ext-dom": "*", 36 | "ext-xdebug": ">=2.0.5" 37 | }, 38 | "type": "library", 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "1.2.x-dev" 42 | } 43 | }, 44 | "autoload": { 45 | "classmap": [ 46 | "PHP/" 47 | ] 48 | }, 49 | "notification-url": "https://packagist.org/downloads/", 50 | "include-path": [ 51 | "" 52 | ], 53 | "license": [ 54 | "BSD-3-Clause" 55 | ], 56 | "authors": [ 57 | { 58 | "name": "Sebastian Bergmann", 59 | "email": "sb@sebastian-bergmann.de", 60 | "role": "lead" 61 | } 62 | ], 63 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 64 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 65 | "keywords": [ 66 | "coverage", 67 | "testing", 68 | "xunit" 69 | ], 70 | "time": "2013-07-06 06:26:16" 71 | }, 72 | { 73 | "name": "phpunit/php-file-iterator", 74 | "version": "1.3.3", 75 | "source": { 76 | "type": "git", 77 | "url": "git://github.com/sebastianbergmann/php-file-iterator.git", 78 | "reference": "1.3.3" 79 | }, 80 | "dist": { 81 | "type": "zip", 82 | "url": "https://github.com/sebastianbergmann/php-file-iterator/zipball/1.3.3", 83 | "reference": "1.3.3", 84 | "shasum": "" 85 | }, 86 | "require": { 87 | "php": ">=5.3.3" 88 | }, 89 | "type": "library", 90 | "autoload": { 91 | "classmap": [ 92 | "File/" 93 | ] 94 | }, 95 | "notification-url": "https://packagist.org/downloads/", 96 | "include-path": [ 97 | "" 98 | ], 99 | "license": [ 100 | "BSD-3-Clause" 101 | ], 102 | "authors": [ 103 | { 104 | "name": "Sebastian Bergmann", 105 | "email": "sb@sebastian-bergmann.de", 106 | "role": "lead" 107 | } 108 | ], 109 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 110 | "homepage": "http://www.phpunit.de/", 111 | "keywords": [ 112 | "filesystem", 113 | "iterator" 114 | ], 115 | "time": "2012-10-11 04:44:38" 116 | }, 117 | { 118 | "name": "phpunit/php-text-template", 119 | "version": "1.1.4", 120 | "source": { 121 | "type": "git", 122 | "url": "git://github.com/sebastianbergmann/php-text-template.git", 123 | "reference": "1.1.4" 124 | }, 125 | "dist": { 126 | "type": "zip", 127 | "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4", 128 | "reference": "1.1.4", 129 | "shasum": "" 130 | }, 131 | "require": { 132 | "php": ">=5.3.3" 133 | }, 134 | "type": "library", 135 | "autoload": { 136 | "classmap": [ 137 | "Text/" 138 | ] 139 | }, 140 | "notification-url": "https://packagist.org/downloads/", 141 | "include-path": [ 142 | "" 143 | ], 144 | "license": [ 145 | "BSD-3-Clause" 146 | ], 147 | "authors": [ 148 | { 149 | "name": "Sebastian Bergmann", 150 | "email": "sb@sebastian-bergmann.de", 151 | "role": "lead" 152 | } 153 | ], 154 | "description": "Simple template engine.", 155 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 156 | "keywords": [ 157 | "template" 158 | ], 159 | "time": "2012-10-31 11:15:28" 160 | }, 161 | { 162 | "name": "phpunit/php-timer", 163 | "version": "1.0.4", 164 | "source": { 165 | "type": "git", 166 | "url": "git://github.com/sebastianbergmann/php-timer.git", 167 | "reference": "1.0.4" 168 | }, 169 | "dist": { 170 | "type": "zip", 171 | "url": "https://github.com/sebastianbergmann/php-timer/zipball/1.0.4", 172 | "reference": "1.0.4", 173 | "shasum": "" 174 | }, 175 | "require": { 176 | "php": ">=5.3.3" 177 | }, 178 | "type": "library", 179 | "autoload": { 180 | "classmap": [ 181 | "PHP/" 182 | ] 183 | }, 184 | "notification-url": "https://packagist.org/downloads/", 185 | "include-path": [ 186 | "" 187 | ], 188 | "license": [ 189 | "BSD-3-Clause" 190 | ], 191 | "authors": [ 192 | { 193 | "name": "Sebastian Bergmann", 194 | "email": "sb@sebastian-bergmann.de", 195 | "role": "lead" 196 | } 197 | ], 198 | "description": "Utility class for timing", 199 | "homepage": "http://www.phpunit.de/", 200 | "keywords": [ 201 | "timer" 202 | ], 203 | "time": "2012-10-11 04:45:58" 204 | }, 205 | { 206 | "name": "phpunit/php-token-stream", 207 | "version": "1.1.7", 208 | "source": { 209 | "type": "git", 210 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 211 | "reference": "1.1.7" 212 | }, 213 | "dist": { 214 | "type": "zip", 215 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1.1.7", 216 | "reference": "1.1.7", 217 | "shasum": "" 218 | }, 219 | "require": { 220 | "ext-tokenizer": "*", 221 | "php": ">=5.3.3" 222 | }, 223 | "type": "library", 224 | "autoload": { 225 | "classmap": [ 226 | "PHP/" 227 | ] 228 | }, 229 | "notification-url": "https://packagist.org/downloads/", 230 | "include-path": [ 231 | "" 232 | ], 233 | "license": [ 234 | "BSD-3-Clause" 235 | ], 236 | "authors": [ 237 | { 238 | "name": "Sebastian Bergmann", 239 | "email": "sb@sebastian-bergmann.de", 240 | "role": "lead" 241 | } 242 | ], 243 | "description": "Wrapper around PHP's tokenizer extension.", 244 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 245 | "keywords": [ 246 | "tokenizer" 247 | ], 248 | "time": "2013-07-29 14:27:06" 249 | }, 250 | { 251 | "name": "phpunit/phpunit", 252 | "version": "3.7.22", 253 | "source": { 254 | "type": "git", 255 | "url": "https://github.com/sebastianbergmann/phpunit.git", 256 | "reference": "3.7.22" 257 | }, 258 | "dist": { 259 | "type": "zip", 260 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.22", 261 | "reference": "3.7.22", 262 | "shasum": "" 263 | }, 264 | "require": { 265 | "ext-dom": "*", 266 | "ext-pcre": "*", 267 | "ext-reflection": "*", 268 | "ext-spl": "*", 269 | "php": ">=5.3.3", 270 | "phpunit/php-code-coverage": "~1.2.1", 271 | "phpunit/php-file-iterator": ">=1.3.1", 272 | "phpunit/php-text-template": ">=1.1.1", 273 | "phpunit/php-timer": "~1.0.2", 274 | "phpunit/phpunit-mock-objects": "~1.2.0", 275 | "symfony/yaml": "~2.0" 276 | }, 277 | "require-dev": { 278 | "pear-pear/pear": "1.9.4" 279 | }, 280 | "suggest": { 281 | "ext-json": "*", 282 | "ext-simplexml": "*", 283 | "ext-tokenizer": "*", 284 | "phpunit/php-invoker": ">=1.1.0,<1.2.0" 285 | }, 286 | "bin": [ 287 | "composer/bin/phpunit" 288 | ], 289 | "type": "library", 290 | "extra": { 291 | "branch-alias": { 292 | "dev-master": "3.7.x-dev" 293 | } 294 | }, 295 | "autoload": { 296 | "classmap": [ 297 | "PHPUnit/" 298 | ] 299 | }, 300 | "notification-url": "https://packagist.org/downloads/", 301 | "include-path": [ 302 | "", 303 | "../../symfony/yaml/" 304 | ], 305 | "license": [ 306 | "BSD-3-Clause" 307 | ], 308 | "authors": [ 309 | { 310 | "name": "Sebastian Bergmann", 311 | "email": "sebastian@phpunit.de", 312 | "role": "lead" 313 | } 314 | ], 315 | "description": "The PHP Unit Testing framework.", 316 | "homepage": "http://www.phpunit.de/", 317 | "keywords": [ 318 | "phpunit", 319 | "testing", 320 | "xunit" 321 | ], 322 | "time": "2013-07-06 06:29:15" 323 | }, 324 | { 325 | "name": "phpunit/phpunit-mock-objects", 326 | "version": "1.2.3", 327 | "source": { 328 | "type": "git", 329 | "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", 330 | "reference": "1.2.3" 331 | }, 332 | "dist": { 333 | "type": "zip", 334 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", 335 | "reference": "1.2.3", 336 | "shasum": "" 337 | }, 338 | "require": { 339 | "php": ">=5.3.3", 340 | "phpunit/php-text-template": ">=1.1.1@stable" 341 | }, 342 | "suggest": { 343 | "ext-soap": "*" 344 | }, 345 | "type": "library", 346 | "autoload": { 347 | "classmap": [ 348 | "PHPUnit/" 349 | ] 350 | }, 351 | "notification-url": "https://packagist.org/downloads/", 352 | "include-path": [ 353 | "" 354 | ], 355 | "license": [ 356 | "BSD-3-Clause" 357 | ], 358 | "authors": [ 359 | { 360 | "name": "Sebastian Bergmann", 361 | "email": "sb@sebastian-bergmann.de", 362 | "role": "lead" 363 | } 364 | ], 365 | "description": "Mock Object library for PHPUnit", 366 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 367 | "keywords": [ 368 | "mock", 369 | "xunit" 370 | ], 371 | "time": "2013-01-13 10:24:48" 372 | }, 373 | { 374 | "name": "symfony/yaml", 375 | "version": "v2.3.2", 376 | "target-dir": "Symfony/Component/Yaml", 377 | "source": { 378 | "type": "git", 379 | "url": "https://github.com/symfony/Yaml.git", 380 | "reference": "v2.3.2" 381 | }, 382 | "dist": { 383 | "type": "zip", 384 | "url": "https://api.github.com/repos/symfony/Yaml/zipball/v2.3.2", 385 | "reference": "v2.3.2", 386 | "shasum": "" 387 | }, 388 | "require": { 389 | "php": ">=5.3.3" 390 | }, 391 | "type": "library", 392 | "extra": { 393 | "branch-alias": { 394 | "dev-master": "2.3-dev" 395 | } 396 | }, 397 | "autoload": { 398 | "psr-0": { 399 | "Symfony\\Component\\Yaml\\": "" 400 | } 401 | }, 402 | "notification-url": "https://packagist.org/downloads/", 403 | "license": [ 404 | "MIT" 405 | ], 406 | "authors": [ 407 | { 408 | "name": "Fabien Potencier", 409 | "email": "fabien@symfony.com" 410 | }, 411 | { 412 | "name": "Symfony Community", 413 | "homepage": "http://symfony.com/contributors" 414 | } 415 | ], 416 | "description": "Symfony Yaml Component", 417 | "homepage": "http://symfony.com", 418 | "time": "2013-07-11 19:36:36" 419 | } 420 | ], 421 | "aliases": [ 422 | 423 | ], 424 | "minimum-stability": "stable", 425 | "stability-flags": [ 426 | 427 | ], 428 | "platform": [ 429 | 430 | ], 431 | "platform-dev": [ 432 | 433 | ] 434 | } 435 | -------------------------------------------------------------------------------- /MongoSession.php: -------------------------------------------------------------------------------- 1 | 14 | * MongoSession::config(array( 15 | * 'connection' => 'mongodb://localhost:27017', 16 | * 'cookie_domain' => $_SERVER['HOST_NAME'] 17 | * )); 18 | * 19 | * 20 | * Then call the init: 21 | * 22 | * MongoSession::init(); 23 | * 24 | * 25 | * Then you can do beautiful things like session_start() or $_SESSION['coolest'] = 'MongoSession!'; 26 | * 27 | */ 28 | class MongoSession 29 | { 30 | /** 31 | * Using singleton pattern, so here's the instance. 32 | * @var MongoSession 33 | */ 34 | private static $instance; 35 | 36 | /** 37 | * The default configuration. 38 | * 39 | * Note that the 'cache' value needs to be carefully considered. 40 | * The value private_no_expire is not the default PHP setting so 41 | * carefully consider what's the most appropriate setting for your 42 | * application. 43 | * 44 | * The 'cookie_domain' should just be set to $_SERVER['HTTP_HOST'] 45 | * unless you have load balancing where a different host is being passed. 46 | */ 47 | private static $config = array( 48 | 'name' => 'PHPSESSID', 49 | 'connection' => 'mongodb://localhost:27017', 50 | 'connection_opts' => array(),//options to pass to MongoClient 51 | 'db' => 'mySessDb', 52 | 'collection' => 'sessions', 53 | 'lockcollection' => 'sessions_lock', 54 | 'timeout' => 3600,//seconds 55 | 'cache' => 'private_no_expire', 56 | 'cache_expiry' => 10,//minutes 57 | 'cookie_path' => '/', 58 | 'cookie_domain' => '.thisdomain.com', 59 | 'cookie_secure' => false, 60 | 'cookie_httponly' => false, 61 | 'autostart' => false, 62 | 'locktimeout' => 30,//seconds 63 | 'locksleep' => 100,//milliseconds 64 | 'cleanonclose' => false,//this is an option for testing purposes 65 | 'error_handler' => 'trigger_error', 66 | 'logger' => false,//by default, no logging 67 | 'machine_id' => false,//identify the machine, if you want for debugs 68 | 'write_concern' => 1,//by default, MongoClient uses w=1 (Mongo 'safe' mode) 69 | 'write_journal' => false,//by default, no journaling required before ack 70 | ); 71 | 72 | /** 73 | * The instance configuration. 74 | * @var array 75 | */ 76 | private $instConfig; 77 | 78 | 79 | /** 80 | * MongoDB connection object. 81 | * @var Mongo 82 | */ 83 | private $conn; 84 | 85 | /** 86 | * The database where the data is stored. 87 | * @var MongoDB 88 | */ 89 | private $db; 90 | 91 | /** 92 | * The session collection, the actual name is specified 93 | * in the configuration. 94 | * @var MongoCollection 95 | */ 96 | private $sessions; 97 | 98 | /** 99 | * The lock collection, actual name found in config. 100 | * @var MongoCollection 101 | */ 102 | private $locks; 103 | 104 | /** 105 | * The session ID which is saved since php doesn't pass the session ID 106 | * when calling the close method. Also this is used to detect when a 107 | * session is being regenerated. 108 | * @var string 109 | */ 110 | private $sid; 111 | 112 | /** 113 | * Storing the current session document. 114 | * @var array 115 | */ 116 | private $sessionDoc; 117 | 118 | /** 119 | * Indicates whether this client acquired the lock or not. This 120 | * is used to determine whether this instance can release a lock 121 | * or not. Only if the lock was acquired in this instance is this 122 | * allowed. 123 | * @var boolean 124 | */ 125 | private $lockAcquired = false; 126 | 127 | /** 128 | * Set the configuration. 129 | * @var $config array 130 | * @return null 131 | */ 132 | public static function config(array $config = array()) 133 | { 134 | //configs 135 | self::$config = array_merge(self::$config, $config); 136 | } 137 | 138 | /** 139 | * Get the instance, or set up a new one. 140 | * @return MongoSession 141 | */ 142 | public static function instance() 143 | { 144 | if (self::$instance) { 145 | return self::$instance; 146 | } 147 | 148 | //set up a proper instance 149 | self::$instance = new self; 150 | 151 | return self::$instance; 152 | } 153 | 154 | /** 155 | * Need to call this method to start sessions. 156 | * @param boolean $dbInit When passing true, it will also call ensureIndex() 157 | * on the appropriate collections so that Mongo isn't 158 | * slow. You should never pass true in a production app. 159 | * It should only be called once, perhaps by an install 160 | * script. 161 | * @return null 162 | */ 163 | public static function init($dbInit = false) 164 | { 165 | $i = self::instance(); 166 | 167 | if ($dbInit) { 168 | $i->dbInit(); 169 | } 170 | 171 | } 172 | 173 | /** 174 | * Private constructor to satisfy the singleton design pattern. You should 175 | * be calling MongoSession::init() prior to starting sessions. 176 | */ 177 | private function __construct() 178 | { 179 | //set the configs 180 | $this->setConfig(self::$config); 181 | 182 | //set the cookie settings 183 | session_set_cookie_params(0, $this->getConfig('cookie_path'), $this->getConfig('cookie_domain'), 184 | $this->getConfig('cookie_secure'), $this->getConfig('cookie_httponly')); 185 | 186 | //set HTTP cache headers 187 | session_cache_limiter($this->getConfig('cache')); 188 | session_cache_expire($this->getConfig('cache_expiry')); 189 | 190 | //we need to ensure that PHP knows about our explicit timeout 191 | ini_set('session.gc_maxlifetime', $this->getConfig('lifetime')); 192 | 193 | //Mongo/MongoClient( uri, options ) 194 | $mongo_options = array(); 195 | foreach ($this->getConfig('connection_opts') as $optname=>$optvalue) { 196 | $mongo_options[$optname] = $optvalue; 197 | } 198 | 199 | //Mongo() defunct, use MongoClient() if available 200 | $mongo_class = ( (class_exists('MongoClient')) ? ('MongoClient') : ('Mongo') ); 201 | $this->conn = new $mongo_class( 202 | $this->getConfig('connection'), 203 | $mongo_options 204 | ); 205 | 206 | if ($mongo_class == 'MongoClient') { 207 | //set write concern from config 208 | $this->instConfig['write_options'] = array('w'=>$this->getConfig('write_concern'), 'j'=>$this->getConfig('write_journal')); 209 | } else { 210 | //defunct 'safe' write, use safe mode if w > 0 211 | $this->instConfig['write_options'] = array('safe'=>$this->getConfig('write_concern')>0); 212 | } 213 | 214 | //make the connection explicit 215 | $this->conn->connect(); 216 | 217 | //init some variables for use 218 | $db = $this->getConfig('db'); 219 | $coll = $this->getConfig('collection'); 220 | $lock = $this->getConfig('lockcollection'); 221 | 222 | //connect to the db and collections 223 | $this->db = $this->conn->$db; 224 | $this->sessions = $this->db->$coll; 225 | $this->locks = $this->db->$lock; 226 | 227 | //tell PHP to use this class as the handler 228 | session_set_save_handler( 229 | array($this, 'open'), 230 | array($this, 'close'), 231 | array($this, 'read'), 232 | array($this, 'write'), 233 | array($this, 'destroy'), 234 | array($this, 'gc') 235 | ); 236 | } 237 | 238 | /** 239 | * Builds indices on the appropriate collections. No need to call directly. 240 | */ 241 | public function dbInit() 242 | { 243 | $mongo_index = ( (phpversion('mongo') >= '1.5.0') ? ('createIndex') : ('ensureIndex') ); 244 | $this->log("maint: {$mongo_index} on ".$this->getConfig('collection')); 245 | $this->sessions->$mongo_index(array( 246 | 'last_accessed' => 1 247 | )); 248 | $this->log("maint: {$mongo_index} on ".$this->getConfig('lockcollection')); 249 | $this->locks->$mongo_index(array( 250 | 'created' => 1 251 | )); 252 | } 253 | 254 | /** 255 | * Set the configuration array for this instance. 256 | * @param array $config The configuration array (see static::$config for format) 257 | */ 258 | private function setConfig(array $config) 259 | { 260 | $this->instConfig = $config; 261 | } 262 | 263 | /** 264 | * Get a configuration item. Will return null if it doesn't exist. 265 | * @var $key string The key of the configuration you're looking. 266 | * @return mixed 267 | */ 268 | private function getConfig($key) 269 | { 270 | if (!array_key_exists($key, $this->instConfig)) 271 | return null; 272 | else 273 | return $this->instConfig[$key]; 274 | } 275 | 276 | /** 277 | * Acquires a lock on a session, or it waits for a specified amount of time 278 | * WARNING: This method isn't expected to fail in any realistic application. 279 | * In the case of a tiny Mongo server with tons of web traffic, it's conceivable 280 | * that this method could fail. Keep in mind that php will 281 | * make sure that write() and close() is also called if this fails. There's no 282 | * specific way to ensure that this never fails since it's dependent on the 283 | * application design. Overall, one should be extremely careful with making 284 | * sure that the Mongo database can handle the load you'll be sending its way. 285 | * 286 | * @param string $sid The session ID to acquire a lock on. 287 | * @return boolean True if succeeded, false if not. 288 | */ 289 | private function lock($sid) 290 | { 291 | //check if we've already acquired a lock 292 | if ($this->lockAcquired) return true; 293 | 294 | $timeout = $this->getConfig('locktimeout') * 1000000;//microseconds we want 295 | $sleep = $this->getConfig('locksleep') * 1000;//we want microseconds 296 | $start = microtime(true); 297 | 298 | $this->log('Trying to acquire a lock on ' . $sid); 299 | 300 | $waited = false; 301 | 302 | do { 303 | //check if there is a current lock 304 | $lock = $this->locks->findOne(array('_id' => $sid)); 305 | 306 | if (!$lock) { 307 | $lock = array(); 308 | $lock['_id'] = $sid; 309 | $lock['created'] = new MongoDate(); 310 | 311 | if ($mid = $this->getConfig('machine_id')) 312 | $lock['mid'] = $mid; 313 | 314 | try { 315 | $res = $this->locks->insert($lock, $this->getConfig('write_options')); 316 | } catch (MongoDuplicateKeyException $e) { 317 | //duplicate key may occur during lock race 318 | continue; 319 | } catch (MongoCursorException $e) { 320 | if (in_array($e->getCode(), array(11001, 11000, 12582))) { 321 | //catch duplicate key if no exception thrown 322 | continue; 323 | } elseif (preg_match('/replication timed out/i', $e->getMessage())) { 324 | //replication error, to avoid partial write/lockout override write concern and unlock before error 325 | $this->instConfig['write_options'] = ( (class_exists('MongoClient')) ? (array('w'=>0)) : (array('safe'=>false)) ); 326 | //force unlock to prevent lockout from partial write 327 | $this->unlock($sid, true); 328 | } 329 | //log exception and fail lock 330 | $this->log('exception: ' . $e->getMessage()); 331 | break 1; 332 | } 333 | 334 | $this->lockAcquired = true; 335 | 336 | $this->log('Lock acquired @ ' . date('Y-m-d H:i:s', $lock['created']->sec)); 337 | 338 | if ($waited) 339 | $this->log('LOCK_WAIT_SECONDS:' . number_format(microtime(true) - $start, 5)); 340 | 341 | return true; 342 | } 343 | 344 | //we need to sleep 345 | usleep($sleep); 346 | $waited = true; 347 | $timeout -= $sleep; 348 | } while ($timeout > 0); 349 | 350 | //no lock could be acquired, so try to use an error handler for this 351 | $this->errorHandler('Could not acquire lock for ' . $sid); 352 | } 353 | 354 | /** 355 | * Release lock **only** if this instance had acquired it. 356 | * @param string $sid The session ID that php passes. 357 | */ 358 | private function unlock($sid, $force=false) 359 | { 360 | if ($this->lockAcquired || $force) { 361 | $this->lockAcquired = false; 362 | $this->locks->remove(array('_id' => $sid), $this->getConfig('write_options')); 363 | } 364 | } 365 | 366 | /** 367 | * A useless method since this is where file handling would occur, except there's 368 | * no files to open and the database connection was opened in the constructor, 369 | * so this just needs to exist but doesn't actually do anythiing. 370 | * @param string $path The storage path that php passes. Not relevant to Mongo. 371 | * @param string $name The name of the session, defaults to PHPSESSID but could be anything. 372 | * @return boolean Always true. 373 | */ 374 | public function open($path, $name) 375 | { 376 | return true; 377 | } 378 | 379 | /** 380 | * Closes the session. Invoked by PHP but doesn't pass the session ID, so we use the session 381 | * ID that we previously saved in open/write. During testing, one could also invoke garbage 382 | * collection by setting 'cleanonclose' setting to true. This is only useful to test garbage 383 | * collection, but on production you shouldn't be doing that on every run. 384 | * @return boolean true 385 | */ 386 | public function close() 387 | { 388 | //release any locks 389 | $this->unlock($this->sid); 390 | 391 | //do an explicit gc() if called for 392 | if ($this->getConfig('cleanonclose')) { 393 | $this->gc(); 394 | } 395 | 396 | return true; 397 | } 398 | 399 | /** 400 | * Read the contents of the session. Get's called once during a request to get entire session contents. 401 | * 402 | * @param string $sid The session ID passed by PHP. 403 | * @return string Either an empty string if there's nothing in a session of a special session 404 | * serialized string. In this case we're storing in the DB as MongoBinData since 405 | * UTF-8 is harder to enforce than just storing as binary. 406 | */ 407 | public function read($sid) 408 | { 409 | //save the session ID for closing later 410 | $this->sid = $sid; 411 | 412 | //a lock MUST be acquired, but the complexity is in the lock() method 413 | $this->lock($sid); 414 | 415 | $this->sessionDoc = $this->sessions->findOne(array('_id' => $sid)); 416 | 417 | if (!$this->sessionDoc) { 418 | return ''; 419 | } else { 420 | //return the string data (stored as Mongo binary format) 421 | return $this->sessionDoc['data']->bin; 422 | } 423 | } 424 | 425 | /** 426 | * Save the session data. 427 | * @param string $sid The session ID that PHP passes. 428 | * @param string $data The session serialized data string. 429 | * @return boolean True always. 430 | */ 431 | public function write($sid, /*string*/ $data) 432 | { 433 | //update/insert our session data 434 | if (!$this->sessionDoc) { 435 | $this->sessionDoc = array(); 436 | $this->sessionDoc['_id'] = $sid; 437 | $this->sessionDoc['started'] = new MongoDate(); 438 | } 439 | 440 | //there could have been a session regen so we need to be careful with the $sid here and set it anyway 441 | if ($this->sid != $sid) { 442 | //need to unlock old sid 443 | $this->unlock($this->sid); 444 | 445 | //set the new one 446 | $this->sid = $sid; 447 | $this->lock($this->sid);//@TODO shouldn't we try to see if this succeeded first? 448 | 449 | //and also make sure we're going to write to the correct document 450 | $this->sessionDoc['_id'] = $sid; 451 | } 452 | 453 | $this->sessionDoc['last_accessed'] = new MongoDate(); 454 | $this->sessionDoc['data'] = new MongoBinData($data, MongoBinData::BYTE_ARRAY); 455 | 456 | $this->sessions->save($this->sessionDoc, $this->getConfig('write_options')); 457 | 458 | return true; 459 | } 460 | 461 | /** 462 | * Tries to invoke the error handler specified in settings. 463 | */ 464 | private function errorHandler($msg) 465 | { 466 | $waited = $this->getConfig('locktimeout'); 467 | $this->log("PANIC! {$this->sid} cannot be acquired after waiting for {$waited}s. "); 468 | $h = $this->getConfig('error_handler'); 469 | 470 | //call and exit 471 | call_user_func_array($h, array($msg)); 472 | exit(1); 473 | } 474 | 475 | /** 476 | * For logging, if we want to. 477 | */ 478 | private function log($msg) 479 | { 480 | $logger = $this->getConfig('logger'); 481 | if (!$logger) return false; 482 | return call_user_func_array($logger, array($msg)); 483 | } 484 | 485 | /** 486 | * Destroy the session. 487 | * @param string $sid The session ID to destroy. 488 | * @return boolean True always. 489 | */ 490 | public function destroy($sid) 491 | { 492 | $this->sessions->remove(array('_id' => $sid), $this->getConfig('write_options')); 493 | 494 | return true; 495 | } 496 | 497 | /** 498 | * The garbage collection function invoked by PHP. 499 | * @param int $lifetime The lifetime param, defaults to 1440 seconds in PHP. 500 | * @return boolean True always. 501 | */ 502 | public function gc($lifetime = 0) 503 | { 504 | $timeout = $this->getConfig('timeout'); 505 | 506 | //find all sessions that are older than $timeout 507 | $olderThan = time() - $timeout; 508 | 509 | //no ack required 510 | $this->sessions->remove( 511 | array('last_accessed' => array('$lt' => new MongoDate($olderThan))), 512 | ( (class_exists('MongoClient')) ? (array('w'=>0)) : (array('safe'=>false)) ) 513 | ); 514 | 515 | return true; 516 | } 517 | } 518 | --------------------------------------------------------------------------------