├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── php ├── Handler.php ├── Handlers │ ├── BaseHandler.php │ ├── DefaultHandler.php │ ├── EncryptionHandler.php │ ├── MemoryHandler.php │ └── NoopHandler.php ├── Manager.php └── Objects │ └── MemoryItem.php ├── phpunit.xml └── test └── phpunit ├── Handlers ├── BaseHandlerTest.php ├── MemoryHandlerTest.php └── NoopHandlerTest.php └── Objects └── MemoryItemTest.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Install dependencies 13 | uses: php-actions/composer@v6 14 | with: 15 | version: 1 16 | php_version: 7.4 17 | 18 | - name: PHPStan Static Analysis 19 | uses: php-actions/phpstan@v3 20 | with: 21 | path: php/ 22 | php_version: 7.4 23 | level: 2 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | - nightly 9 | 10 | matrix: 11 | fast_finish: true 12 | allow_failures: 13 | - php: nightly 14 | 15 | before_script: 16 | - composer install --dev --no-interaction 17 | 18 | after_script: 19 | - php vendor/bin/coveralls -v 20 | 21 | 22 | script: 23 | - mkdir -p build/logs 24 | - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml 25 | 26 | after_success: 27 | - travis_retry php vendor/bin/coveralls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 0.4.0 - 2021-07-21 8 | ### Changed 9 | - Add PHPStan static code analysis 10 | - Check the header hasn't been sent for calling `session_set_save_handler()`. Props @pbearne 11 | 12 | ## 0.3.1 - 2018-01-16 13 | ### Changed 14 | - Fix a PHP error during `BaseHandler` wireup. Props @sayful1 15 | 16 | ## 0.3.0 - 2018-01-15 17 | ### Changed 18 | - Updated encryption handler to use a specific `Key` type from Defuse's library 19 | - Updated documentation 20 | - Updated Travis test matrix for modern PHP 21 | 22 | ## 0.2.1 - 2017-04-25 23 | ### Changed 24 | - Fixed a naming issue affecting internal object storage 25 | 26 | ## 0.2.0 - 2017-04-25 27 | ### Changed 28 | - Switched from the PHP doc's crypto example to the Defuse Crypto library 29 | 30 | ## 0.1.0 - 2016-11-26 31 | ### Added 32 | - README explaining the purpose and use of the project 33 | - Tests to verify adequate functionality 34 | - This CHANGELOG file 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eric Mann 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Sessionz [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url] [![CI](https://github.com/ericmann/sessionz/actions/workflows/ci.yml/badge.svg)](https://github.com/ericmann/sessionz/actions/workflows/ci.yml) 2 | 3 | Sessionz is a PHP library for smarter session management in modular applications. 4 | 5 | ## Quick Start 6 | 7 | Use [Composer](https://getcomposer.org/) to add `ericmann/sessionz` to your project. Then, after loading all of your dependencies, initialize the core session manager and add the handlers you need to your stack. 8 | 9 | ``` 10 | require __DIR__ . '/vendor/autoload.php'; 11 | 12 | EAMann\Sessionz\Manager::initialize() 13 | ->addHandler( new \EAMann\Sessionz\Handlers\DefaultHandler() ) 14 | ->addHandler( new \EAMann\Sessionz\Handlers\EncryptionHandler( getenv('session_passkey') ) ) 15 | ->addHandler( new \EAMann\Sessionz\Handlers\MemoryHandler() ) 16 | 17 | session_start(); 18 | 19 | ``` 20 | 21 | The above example adds, in order: 22 | 23 | - The default PHP session handler (which uses files for storage) 24 | - An encryption middleware such that session data will be encrypted at rest on disk 25 | - An in-memory cache to avoid round-trips to the filesystem on read 26 | 27 | ## How Handlers Work 28 | 29 | The session manager maintains a list of registered "handlers" to which it passes requests from the PHP engine to: 30 | 31 | - Read a session 32 | - Write a session 33 | - Create a session store 34 | - Clean up (garbage collect) expired sessions 35 | - Delete a session 36 | 37 | Each handler must implement the `Handler` interface so the session manager knows how to work with them. 38 | 39 | ### Middleware Structure 40 | 41 | The overall structure of the handler stack is identical to that of a middleware stack in a modern PHP application. You can read more about the general philosophy [on Slim's website](https://www.slimframework.com/docs/concepts/middleware.html#how-does-middleware-work). 42 | 43 | In general, stack operations will flow from the outside-in, starting by invoking the appropriate operation on the most recently registered handler and walking down the stack to the oldest handler. Each handler has the option to halt execution and return data immediately, or can invoke the passed `$next` callback to continue operation. 44 | 45 | Using the quick start example above: 46 | 47 | - Requests start in the `MemoryHandler` 48 | - If necessary, they then pass to the `EncryptionHandler` 49 | - Requests always pass from encryption to the `DefaultHandler` 50 | - If necessary, they then pass to the (hidden) `BaseHandler` 51 | - Then everything returns because the base handler doesn't pass anything on 52 | 53 | ## Available Handlers 54 | 55 | ### `DefaultHandler` 56 | 57 | The default session handler merely exposes PHP's default session implementation to our custom manager. Including this handler will provide otherwise standard PHP session functionality to the project as a whole, but this functionality can be extended by placing other stacks on top. 58 | 59 | ### `EncryptionHandler` 60 | 61 | Sessions stored on disk (the default implementation) or in a separate storage system (Memcache, MySQL, or similar) should be encrypted _at rest_. This handler will automatically encrypt any information passing through it on write and decrypt data on read. It does not store data on its own. 62 | 63 | This handler requires a symmetric encryption key when it's instantiated. This key should be an ASCII-safe string, 32 bytes in length. You can easily use [Defuse PHP Encryption](https://github.com/defuse/php-encryption) (a dependency of this library) to generate a new key: 64 | 65 | ```php 66 | $rawKey = Defuse\Crypto\Key::createNewRandomKey(); 67 | $key = $rawKey->saveToAsciiSafeString(); 68 | ``` 69 | 70 | ### `MemoryHandler` 71 | 72 | If the final storage system presented to the session manager is remote, reads and writes can take a non-trivial amount of time. Storing session data in memory helps to make the application more performant. Reads will stop at this layer in the stack if the session is found (i.e. the cache is hot) but will flow to the next layer if no session exists. When a session is found in a subsequent layer, this handler will update its cache to make the data available upon the next lookup. 73 | 74 | Writes will update the cache and pass through to the next layer in the stack. 75 | 76 | ### Abstract handlers 77 | 78 | The `BaseHandler` class is always instantiated and included at the root of the handler stack by default. This is so that, no matter what handlers you add in to the stack, the session manager will always return a standard, reliable set of information. 79 | 80 | The `NoopHandler` class is provided for you to build additional middleware atop a standard interface that "passes through" to the next layer in the stack by default. The `EncryptionHandler`, for example, inherits from this class as it doesn't store or read data, but merely manipulates information before passing it along. Another implementation might be a logging interface to track when sessions are accessed/updated. 81 | 82 | ## Credits 83 | 84 | The middleware implementation is inspired heavily by the request middleware stack presented by [the Slim Framework](https://www.slimframework.com/). 85 | 86 | [travis-image]: https://travis-ci.org/ericmann/sessionz.svg?branch=master 87 | [travis-url]: https://travis-ci.org/ericmann/sessionz 88 | [coveralls-image]: https://coveralls.io/repos/github/ericmann/sessionz/badge.svg?branch=master 89 | [coveralls-url]: https://coveralls.io/github/ericmann/sessionz?branch=master 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ericmann/sessionz", 3 | "description": "PHP Session Manager Interface", 4 | "type": "library", 5 | "version": "0.3.1", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Eric Mann", 10 | "email": "eric@eamann.com" 11 | } 12 | ], 13 | "minimum-stability": "stable", 14 | "autoload": { 15 | "psr-4": { 16 | "EAMann\\Sessionz\\": "php/" 17 | } 18 | }, 19 | "require": { 20 | "php": ">=5.6", 21 | "defuse/php-encryption": "^2.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^5.6", 25 | "satooshi/php-coveralls": "^1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /php/Handler.php: -------------------------------------------------------------------------------- 1 | handler = new \SessionHandler(); 34 | } 35 | 36 | public function __destruct() 37 | { 38 | @$this->handler->close(); 39 | } 40 | 41 | /** 42 | * Delete a session from storage by ID. 43 | * 44 | * @param string $id ID of the session to remove 45 | * @param callable $next Callable to invoke the next layer in the stack 46 | * 47 | * @return bool 48 | */ 49 | public function delete($id, $next) 50 | { 51 | $status = $this->handler->destroy($id); 52 | return $next($id) && $status; 53 | } 54 | 55 | /** 56 | * Clean up all session older than the max lifetime specified. 57 | * 58 | * @param int $maxlifetime Max number of seconds for a valid session 59 | * @param callable $next Callable to invoke the next layer in the stack 60 | * 61 | * @return bool 62 | */ 63 | public function clean($maxlifetime, $next) 64 | { 65 | $status = $this->handler->gc($maxlifetime); 66 | return $next($maxlifetime) && $status; 67 | } 68 | 69 | /** 70 | * Create a new session store. 71 | * 72 | * @param string $path Path where the storage lives 73 | * @param string $name Name of the session store to create 74 | * @param callable $next Callable to invoke the next layer in the stack 75 | * 76 | * @return bool 77 | */ 78 | public function create($path, $name, $next) 79 | { 80 | $status = $this->handler->open($path, $name); 81 | return $next($path, $name) && $status; 82 | } 83 | 84 | /** 85 | * Read a specific session from storage. 86 | * 87 | * @param string $id ID of the session to read 88 | * @param callable $next Callable to invoke the next layer in the stack 89 | * 90 | * @return string 91 | */ 92 | public function read($id, $next) 93 | { 94 | return empty($this->handler->read($id)) ? $next($id) : $this->handler->read($id); 95 | } 96 | 97 | /** 98 | * Write session data to storage. 99 | * 100 | * @param string $id ID of the session to write 101 | * @param string $data Data to be written 102 | * @param callable $next Callable to invoke the next layer in the stack 103 | * 104 | * @return bool 105 | */ 106 | public function write($id, $data, $next) 107 | { 108 | $this->handler->write($id, $data);die; 109 | return $next($id, $data); 110 | } 111 | } -------------------------------------------------------------------------------- /php/Handlers/EncryptionHandler.php: -------------------------------------------------------------------------------- 1 | key = Key::loadFromAsciiSafeString($key); 32 | } 33 | 34 | /** 35 | * Attempt to decrypt the ciphertext passed in given the key supplied by the 36 | * constructor. 37 | * 38 | * @param string $ciphertext 39 | * 40 | * @return string 41 | */ 42 | protected function decrypt($ciphertext) 43 | { 44 | return Crypto::decrypt($ciphertext, $this->key); 45 | } 46 | 47 | /** 48 | * Use the Crypto object from Defuse's library to encrypt the plain text with the key 49 | * supplied in the constructor. 50 | * 51 | * @param string $plaintext 52 | * 53 | * @return string 54 | */ 55 | protected function encrypt($plaintext) 56 | { 57 | return Crypto::encrypt($plaintext, $this->key); 58 | } 59 | 60 | /** 61 | * Read all data from farther down the stack (i.e. earlier-added handlers) 62 | * and then decrypt the data given specified keys. 63 | * 64 | * @param string $id ID of the session to read 65 | * @param callable $next Callable to invoke the next layer in the stack 66 | * 67 | * @return string 68 | */ 69 | public function read($id, $next) 70 | { 71 | $encrypted = $next( $id ); 72 | 73 | return empty( $encrypted ) ? $encrypted : $this->decrypt( $next( $id ) ); 74 | } 75 | 76 | /** 77 | * Encrypt the incoming data payload, then pass it along to the next handler 78 | * in the stack. 79 | * 80 | * @param string $id ID of the session to write 81 | * @param string $data Data to be written 82 | * @param callable $next Callable to invoke the next layer in the stack 83 | * 84 | * @return bool 85 | */ 86 | public function write($id, $data, $next) 87 | { 88 | $return = empty( $data ) ? $data : $this->encrypt( $data ); 89 | return $next( $id, $return ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /php/Handlers/MemoryHandler.php: -------------------------------------------------------------------------------- 1 | cache[$id])) { 40 | /** @var MemoryItem $item */ 41 | $item = $this->cache[$id]; 42 | if (!$item->is_valid()) { 43 | unset($this->cache[$id]); 44 | return false; 45 | } 46 | 47 | return $item->data; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | public function __construct() 54 | { 55 | $this->cache = []; 56 | } 57 | 58 | /** 59 | * Purge an item from the cache immediately. 60 | * 61 | * @param string $id 62 | * @param callable $next 63 | * 64 | * @return mixed 65 | */ 66 | public function delete($id, $next) 67 | { 68 | unset($this->cache[$id]); 69 | return $next($id); 70 | } 71 | 72 | /** 73 | * Update the internal cache by filtering out any items that are no longer valid. 74 | * 75 | * @param int $maxlifetime 76 | * @param callable $next 77 | * 78 | * @codeCoverageIgnore Due to timestamp issues, this is currently untestable ... 79 | * 80 | * @return mixed 81 | */ 82 | public function clean($maxlifetime, $next) 83 | { 84 | $this->cache = array_filter($this->cache, function($item) use ($maxlifetime) { 85 | /** @var MemoryItem $item */ 86 | return $item->is_valid($maxlifetime); 87 | }); 88 | 89 | return $next($maxlifetime); 90 | } 91 | 92 | /** 93 | * Pass things through to the next middleare. This function is a no-op. 94 | * 95 | * @param string $path 96 | * @param string $name 97 | * @param callable $next 98 | * 99 | * @return mixed 100 | */ 101 | public function create($path, $name, $next) 102 | { 103 | return $next($path, $name); 104 | } 105 | 106 | /** 107 | * Grab the item from the cache if it exists, otherwise delve deeper 108 | * into the stack and retrieve from another underlying middlware. 109 | * 110 | * @param string $id 111 | * @param callable $next 112 | * 113 | * @return string 114 | */ 115 | public function read($id, $next) 116 | { 117 | $data = $this->_read($id); 118 | if ( false === $data ) { 119 | $data = $next($id); 120 | if (false !== $data) { 121 | $item = new MemoryItem($data); 122 | $this->cache[$id] = $item; 123 | } 124 | } 125 | 126 | return $data; 127 | } 128 | 129 | /** 130 | * Store the item in the cache and then pass the data, unchanged, down 131 | * the middleware stack. 132 | * 133 | * @param string $id 134 | * @param string $data 135 | * @param callable $next 136 | * 137 | * @return mixed 138 | */ 139 | public function write($id, $data, $next) 140 | { 141 | $item = new MemoryItem($data); 142 | $this->cache[$id] = $item; 143 | 144 | return $next($id, $data); 145 | } 146 | } -------------------------------------------------------------------------------- /php/Handlers/NoopHandler.php: -------------------------------------------------------------------------------- 1 | handlerLock) { 55 | throw new \RuntimeException('Session handlers can’t be added once the stack is dequeuing'); 56 | } 57 | if (is_null($this->handlers)) { 58 | $this->seedHandlerStack(); 59 | } 60 | 61 | // DELETE 62 | $next_delete = $this->stacks['delete']->top(); 63 | $this->stacks['delete'][] = function($session_id) use ($handler, $next_delete) { 64 | return call_user_func(array( $handler, 'delete'), $session_id, $next_delete); 65 | }; 66 | 67 | // CLEAN 68 | $next_clean = $this->stacks['clean']->top(); 69 | $this->stacks['clean'][] = function($lifetime) use ($handler, $next_clean) { 70 | return call_user_func(array( $handler, 'clean'), $lifetime, $next_clean); 71 | }; 72 | 73 | // CREATE 74 | $next_create = $this->stacks['create']->top(); 75 | $this->stacks['create'][] = function($path, $name) use ($handler, $next_create) { 76 | return call_user_func(array( $handler, 'create'), $path, $name, $next_create); 77 | }; 78 | 79 | // READ 80 | $next_read = $this->stacks['read']->top(); 81 | $this->stacks['read'][] = function($session_id) use ($handler, $next_read) { 82 | return call_user_func(array( $handler, 'read'), $session_id, $next_read); 83 | }; 84 | 85 | // WRITE 86 | $next_write = $this->stacks['write']->top(); 87 | $this->stacks['write'][] = function($session_id, $session_data) use ($handler, $next_write) { 88 | return call_user_func(array( $handler, 'write'), $session_id, $session_data, $next_write); 89 | }; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Seed handler stack with first callable 96 | * 97 | * @throws \RuntimeException if the stack is seeded more than once 98 | */ 99 | protected function seedHandlerStack() 100 | { 101 | if (!is_null($this->handlers)) { 102 | throw new \RuntimeException('Handler stacks can only be seeded once.'); 103 | } 104 | $this->stacks = []; 105 | $base = new BaseHandler(); 106 | $this->handlers = [$base]; 107 | 108 | $this->stacks['delete'] = new \SplStack(); 109 | $this->stacks['clean'] = new \SplStack(); 110 | $this->stacks['create'] = new \SplStack(); 111 | $this->stacks['read'] = new \SplStack(); 112 | $this->stacks['write'] = new \SplStack(); 113 | 114 | foreach($this->stacks as $id => $stack) { 115 | $stack->setIteratorMode(\SplDoublyLinkedList::IT_MODE_LIFO | \SplDoublyLinkedList::IT_MODE_KEEP); 116 | $stack[] = array( $base, $id ); 117 | } 118 | } 119 | 120 | /** 121 | * Initialize the session manager. 122 | * 123 | * Invoking this function multiple times will reset the manager itself 124 | * and purge any handlers already registered with the system. 125 | * 126 | * @return Manager 127 | */ 128 | public static function initialize() 129 | { 130 | $manager = self::$manager = new self(); 131 | $manager->seedHandlerStack(); 132 | 133 | if( ! headers_sent() ){ 134 | session_set_save_handler($manager); 135 | } 136 | 137 | return $manager; 138 | } 139 | 140 | /** 141 | * Close the current session. 142 | * 143 | * Will iterate through all handlers registered to the manager and 144 | * remove them from the stack. This has the effect of removing the 145 | * objects from scope and triggering their destructors. Any cleanup 146 | * should happen there. 147 | * 148 | * @return true 149 | */ 150 | public function close() 151 | { 152 | $this->handlerLock = true; 153 | 154 | while (count($this->handlers) > 0) { 155 | array_pop($this->handlers); 156 | $this->stacks['delete']->pop(); 157 | $this->stacks['clean']->pop(); 158 | $this->stacks['create']->pop(); 159 | $this->stacks['read']->pop(); 160 | $this->stacks['write']->pop(); 161 | } 162 | 163 | $this->handlerLock = false; 164 | return true; 165 | } 166 | 167 | /** 168 | * Destroy a session by either invalidating it or forcibly removing 169 | * it from session storage. 170 | * 171 | * @param string $session_id ID of the session to destroy. 172 | * 173 | * @return bool 174 | */ 175 | public function destroy($session_id) 176 | { 177 | if (is_null($this->handlers)) { 178 | $this->seedHandlerStack(); 179 | } 180 | 181 | /** @var callable $start */ 182 | $start = $this->stacks['delete']->top(); 183 | $this->handlerLock = true; 184 | $data = $start($session_id); 185 | $this->handlerLock = false; 186 | return $data; 187 | } 188 | 189 | /** 190 | * Clean up any potentially expired sessions (sessions with an age 191 | * greater than the specified maximum-allowed lifetime). 192 | * 193 | * @param int $maxlifetime Max number of seconds for which a session is valid. 194 | * 195 | * @return bool 196 | */ 197 | public function gc($maxlifetime) 198 | { 199 | if (is_null($this->handlers)) { 200 | $this->seedHandlerStack(); 201 | } 202 | 203 | /** @var callable $start */ 204 | $start = $this->stacks['clean']->top(); 205 | $this->handlerLock = true; 206 | $data = $start($maxlifetime); 207 | $this->handlerLock = false; 208 | return $data; 209 | } 210 | 211 | /** 212 | * Create a new session storage. 213 | * 214 | * @param string $save_path File location/path where sessions should be written. 215 | * @param string $name Unique name of the storage instance. 216 | * 217 | * @return bool 218 | */ 219 | public function open($save_path, $name) 220 | { 221 | if (is_null($this->handlers)) { 222 | $this->seedHandlerStack(); 223 | } 224 | 225 | /** @var callable $start */ 226 | $start = $this->stacks['create']->top(); 227 | $this->handlerLock = true; 228 | $data = $start($save_path, $name); 229 | $this->handlerLock = false; 230 | return $data; 231 | } 232 | 233 | /** 234 | * Read data from the specified session. 235 | * 236 | * @param string $session_id ID of the session to read. 237 | * 238 | * @return string 239 | */ 240 | public function read($session_id) 241 | { 242 | if (is_null($this->handlers)) { 243 | $this->seedHandlerStack(); 244 | } 245 | 246 | /** @var callable $start */ 247 | $start = $this->stacks['read']->top(); 248 | $this->handlerLock = true; 249 | $data = $start($session_id); 250 | $this->handlerLock = false; 251 | return $data; 252 | } 253 | 254 | /** 255 | * Write data to a specific session. 256 | * 257 | * @param string $session_id ID of the session to write. 258 | * @param string $session_data Serialized string of session data. 259 | * 260 | * @return bool 261 | */ 262 | public function write($session_id, $session_data) 263 | { 264 | if (is_null($this->handlers)) { 265 | $this->seedHandlerStack(); 266 | } 267 | 268 | /** @var callable $start */ 269 | $start = $this->stacks['write']->top(); 270 | $this->handlerLock = true; 271 | $data = $start($session_id, $session_data); 272 | $this->handlerLock = false; 273 | return $data; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /php/Objects/MemoryItem.php: -------------------------------------------------------------------------------- 1 | _data = $data; 35 | $this->_time = null === $time ? time() : (int) $time; 36 | } 37 | 38 | /** 39 | * Magic getter to allow read-only properties 40 | * 41 | * @param string $field 42 | * 43 | * @return mixed 44 | */ 45 | public function __get($field) 46 | { 47 | $field_name = "_$field"; 48 | 49 | return isset($this->$field_name) ? $this->$field_name : null; 50 | } 51 | 52 | /** 53 | * Throw an exception when anyone tries to write anything. 54 | * 55 | * @param string $field 56 | * @param mixed $value 57 | * 58 | * @throws \InvalidArgumentException 59 | */ 60 | public function __set($field, $value) 61 | { 62 | throw new \InvalidArgumentException("Field `$field` is read-only!"); 63 | } 64 | 65 | /** 66 | * Test whether an item is still valid 67 | * 68 | * @param int $lifetime 69 | * @param int $now 70 | * 71 | * @return bool 72 | */ 73 | public function is_valid($lifetime = null, $now = null) 74 | { 75 | if (null === $now) $now = time(); 76 | if (null === $lifetime) $lifetime = ini_get('session.gc_maxlifetime'); 77 | 78 | return (int) $now - $this->_time < (int) $lifetime; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | test/phpunit 13 | 14 | 15 | 16 | 17 | php 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/phpunit/Handlers/BaseHandlerTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($handler->delete('doesntmatter')); 18 | } 19 | 20 | public function test_clean() 21 | { 22 | $handler = new BaseHandler(); 23 | 24 | $this->assertTrue($handler->clean(0)); 25 | } 26 | 27 | public function test_create() 28 | { 29 | $handler = new BaseHandler(); 30 | 31 | $this->assertTrue($handler->create('path', 'name')); 32 | } 33 | 34 | public function test_read() 35 | { 36 | $handler = new BaseHandler(); 37 | 38 | $this->assertEquals('', $handler->read('doesntmatter')); 39 | } 40 | 41 | public function test_write() 42 | { 43 | $handler = new BaseHandler(); 44 | 45 | $this->assertTrue($handler->write('someid', 'data')); 46 | $this->assertEquals('', $handler->read('someid')); 47 | } 48 | } -------------------------------------------------------------------------------- /test/phpunit/Handlers/MemoryHandlerTest.php: -------------------------------------------------------------------------------- 1 | write('exists', 'data', function($id, $data) { return true; }); 15 | 16 | $this->assertEquals('data', $handler->read('exists', function($id) { return ''; })); 17 | 18 | $this->assertTrue($handler->delete('exists', function($id) { return true; })); 19 | 20 | $this->assertEquals('', $handler->read('exists', function($id) { return ''; })); 21 | } 22 | 23 | public function test_create() 24 | { 25 | $handler = new MemoryHandler(); 26 | 27 | $this->assertTrue($handler->create('path', 'name', function($path, $name) { return true; })); 28 | } 29 | 30 | public function test_read_write() 31 | { 32 | $handler = new MemoryHandler(); 33 | $handler->write('exists', 'data', function($id, $data) { return true; }); 34 | 35 | $this->assertEquals('data', $handler->read('exists', function($id) {return ''; })); 36 | $this->assertEquals('', $handler->read('doesntexist', function($id) {return ''; })); 37 | } 38 | } -------------------------------------------------------------------------------- /test/phpunit/Handlers/NoopHandlerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('doesntmatter', $id); 24 | $called = true; 25 | 26 | return true; 27 | }; 28 | 29 | $this->assertTrue($handler->delete('doesntmatter', $callback)); 30 | 31 | $this->assertTrue($called); 32 | } 33 | 34 | public function test_clean() 35 | { 36 | $handler = new ConcreteHandler(); 37 | $called = false; 38 | 39 | $callback = function($lifetime) use (&$called) { 40 | $this->assertEquals(50, $lifetime); 41 | $called = true; 42 | 43 | return true; 44 | }; 45 | 46 | $this->assertTrue($handler->clean(50, $callback)); 47 | 48 | $this->assertTrue($called); 49 | } 50 | 51 | public function test_create() 52 | { 53 | $handler = new ConcreteHandler(); 54 | $called = false; 55 | 56 | $callback = function($path, $name) use (&$called) { 57 | $this->assertEquals('path', $path); 58 | $this->assertEquals('name', $name); 59 | $called = true; 60 | 61 | return true; 62 | }; 63 | 64 | $this->assertTrue($handler->create('path', 'name', $callback)); 65 | 66 | $this->assertTrue($called); 67 | } 68 | 69 | public function test_read() 70 | { 71 | $handler = new ConcreteHandler(); 72 | $called = false; 73 | 74 | $callback = function($id) use (&$called) { 75 | $this->assertEquals('doesntmatter', $id); 76 | $called = true; 77 | 78 | return 'data'; 79 | }; 80 | 81 | $this->assertEquals('data', $handler->read('doesntmatter', $callback)); 82 | 83 | $this->assertTrue($called); 84 | } 85 | 86 | public function test_write() 87 | { 88 | $handler = new ConcreteHandler(); 89 | $called = false; 90 | 91 | $callback = function($id, $data) use (&$called) { 92 | $this->assertEquals('someid', $id); 93 | $this->assertEquals('data', $data); 94 | $called = true; 95 | 96 | return true; 97 | }; 98 | 99 | $this->assertTrue($handler->write('someid', 'data', $callback)); 100 | 101 | $this->assertTrue($called); 102 | } 103 | } -------------------------------------------------------------------------------- /test/phpunit/Objects/MemoryItemTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("data", $item->data); 12 | $this->assertEquals(12345, $item->time); 13 | } 14 | 15 | public function test_data_readonly() 16 | { 17 | $this->expectException(\InvalidArgumentException::class); 18 | 19 | $item = new MemoryItem("data", 12345); 20 | 21 | // Writing data should throw the exception expected above 22 | $item->data = "modified"; 23 | } 24 | 25 | public function test_time_readonly() 26 | { 27 | $this->expectException(\InvalidArgumentException::class); 28 | 29 | $item = new MemoryItem("data", 12345); 30 | 31 | // Writing data should throw the exception expected above 32 | $item->time = 23456; 33 | } 34 | 35 | public function test_validity() { 36 | $item = new MemoryItem("data", 12345); 37 | 38 | // Issued now 39 | $this->assertTrue($item->is_valid(100, 12345)); 40 | 41 | // Issued within expiration 42 | $this->assertTrue($item->is_valid(100, 12350)); 43 | 44 | // Issued too long ago 45 | $this->assertFalse($item->is_valid(100, 15000)); 46 | } 47 | } --------------------------------------------------------------------------------