├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Atyagi │ └── Elasticache │ ├── ElasticacheConnector.php │ ├── ElasticacheServiceProvider.php │ └── ElasticacheSessionHandler.php └── tests ├── ElasticacheSessionHandlerTest.php ├── TestCase.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .idea/ 5 | build/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | 4 | php: 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | 9 | before_script: 10 | - composer self-update 11 | - echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 12 | - composer install --prefer-source --no-interaction --dev 13 | 14 | script: 15 | - mkdir -p build/logs 16 | - phpunit 17 | 18 | after_script: 19 | - php vendor/bin/coveralls -v 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ankit Tyagi 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Elasticache Laravel 2 | =================== 3 | [![Build Status](https://travis-ci.org/atyagi/elasticache-laravel.svg?branch=master)](https://travis-ci.org/atyagi/elasticache-laravel) 4 | [![Coverage Status](https://img.shields.io/coveralls/atyagi/elasticache-laravel.svg?style=flat)](https://coveralls.io/r/atyagi/elasticache-laravel?branch=master) 5 | [![Packagist](http://img.shields.io/packagist/v/atyagi/elasticache-laravel.svg?style=flat)](https://packagist.org/packages/atyagi/elasticache-laravel) 6 | 7 | AWS Elasticache Session and Cache Drivers for Laravel (Memcached specifically) 8 | 9 | ## Setup 10 | 11 | This package requires the memcached extension for PHP. Please see [this link](http://php.net/manual/en/book.memcached.php) for installation instructions. 12 | 13 | With composer, simply add `"atyagi/elasticache-laravel": "~2.1"` to your composer.json. (or `"atyagi/elasticache-laravel": "~1.1"` for Laravel 4 installations) 14 | 15 | Once `composer update` is ran, add 16 | 17 | `'Atyagi\Elasticache\ElasticacheServiceProvider',` 18 | 19 | to the providers array in `app/config.php`. 20 | 21 | At this point, inside of `app/session.php` and `app/cache.php`, you can use `elasticache` as a valid driver. 22 | 23 | #### Versions 24 | - 2.* represents all versions for Laravel 5 25 | - 1.* represents all versions for Laravel 4 26 | 27 | ## Configuration 28 | 29 | All configuration lives within `app/session.php` and `app/cache.php`. The key ones are below: 30 | 31 | #### session.php 32 | - lifetime -- the session lifetime within the Memcached environment 33 | - cookie -- this is the prefix for the session ID to prevent clashing 34 | 35 | #### cache.php 36 | Note: for Laravel 5, make sure to add this info to the stores array as follows: 37 | ````php 38 | 'stores' => [ 39 | ... 40 | 'memcached' => [ 41 | 'driver' => 'memcached', 42 | 'servers' => [ 43 | [ 44 | 'host' => '', 45 | 'port' => '', 46 | 'weight' => '' 47 | ] 48 | ] 49 | ] 50 | ... 51 | ] 52 | ```` 53 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atyagi/elasticache-laravel", 3 | "description": "Elasticache Drivers for Laravel Cache and Session", 4 | "keywords": ["laravel", "AWS", "Elasticache"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Ankit Tyagi", 9 | "email": "ankit.tyagi.2@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4.0", 14 | "illuminate/support": "5.*", 15 | "illuminate/contracts": "5.*" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "4.6.*", 19 | "mockery/mockery": "dev-master", 20 | "satooshi/php-coveralls": "dev-master" 21 | }, 22 | "suggest": { 23 | "ext-memcached": "Leverage the Memcached extension for Elasticache connections." 24 | }, 25 | "autoload": { 26 | "psr-0": { 27 | "Atyagi\\Elasticache": "src/" 28 | } 29 | }, 30 | "minimum-stability": "dev" 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Atyagi/Elasticache/ElasticacheConnector.php: -------------------------------------------------------------------------------- 1 | getMemcached(); 18 | 19 | // Set Elasticache options here 20 | if (defined('\Memcached::OPT_CLIENT_MODE') && defined('\Memcached::DYNAMIC_CLIENT_MODE')) { 21 | $memcached->setOption(\Memcached::OPT_CLIENT_MODE, \Memcached::DYNAMIC_CLIENT_MODE); 22 | } 23 | 24 | // For each server in the array, we'll just extract the configuration and add 25 | // the server to the Memcached connection. Once we have added all of these 26 | // servers we'll verify the connection is successful and return it back. 27 | foreach ($servers as $server) { 28 | $memcached->addServer($server['host'], $server['port'], $server['weight']); 29 | } 30 | 31 | if ($memcached->getVersion() === false) { 32 | throw new \RuntimeException("Could not establish Memcached connection."); 33 | } 34 | 35 | return $memcached; 36 | } 37 | 38 | /** 39 | * Get a new Memcached instance. 40 | * @return \Memcached 41 | */ 42 | protected function getMemcached() 43 | { 44 | return new \Memcached; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Atyagi/Elasticache/ElasticacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make('config'); 22 | 23 | $servers = $config->get('cache.stores.memcached.servers'); 24 | $elasticache = new ElasticacheConnector(); 25 | $memcached = $elasticache->connect($servers); 26 | 27 | // memcached extension not loaded 28 | if ($memcached) { 29 | 30 | $this->app->registerDeferredProvider('Illuminate\Cache\CacheServiceProvider'); 31 | 32 | $this->app->make('session')->extend('elasticache', function () use ($memcached) { 33 | return new ElasticacheSessionHandler($memcached, $this->app); 34 | }); 35 | 36 | $this->app->make('cache')->extend('elasticache', function () use ($memcached, $config) { 37 | /** @noinspection PhpUndefinedNamespaceInspection */ 38 | /** @noinspection PhpUndefinedClassInspection */ 39 | return new \Illuminate\Cache\Repository( 40 | new \Illuminate\Cache\MemcachedStore($memcached, $config->get('cache.prefix'))); 41 | }); 42 | } 43 | } 44 | 45 | /** 46 | * Get the services provided by the provider. 47 | * @return array 48 | */ 49 | public function provides() 50 | { 51 | return []; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Atyagi/Elasticache/ElasticacheSessionHandler.php: -------------------------------------------------------------------------------- 1 | memcached = $memcached; 17 | //force expiry to be in seconds from minutes 18 | $this->sessionExpiry = $app->make('config')->get('session.lifetime') * 60; 19 | $this->sessionPrefix = $app->make('config')->get('session.cookie'); 20 | } 21 | 22 | /** 23 | * Re-initializes existing session, or creates a new one. 24 | * @see http://php.net/sessionhandlerinterface.open 25 | * @param string $savePath Save path 26 | * @param string $sessionName Session name, see http://php.net/function.session-name.php 27 | * @return bool true on success, false on failure 28 | */ 29 | public function open($savePath, $sessionName) 30 | { 31 | if (!is_null($this->memcached)) { 32 | return true; 33 | } 34 | 35 | return false; 36 | } 37 | 38 | /** 39 | * Closes the current session. 40 | * @see http://php.net/sessionhandlerinterface.close 41 | * @return bool true on success, false on failure 42 | */ 43 | public function close() 44 | { 45 | //not necessary for memcached 46 | return true; 47 | } 48 | 49 | /** 50 | * Reads the session data. 51 | * @see http://php.net/sessionhandlerinterface.read 52 | * @param string $sessionId Session ID, see http://php.net/function.session-id 53 | * @return string Same session data as passed in write() or empty string when non-existent or on failure 54 | */ 55 | public function read($sessionId) 56 | { 57 | $sessionId = $this->getSessionId($sessionId); 58 | $value = $this->memcached->get($sessionId); 59 | 60 | return !empty($value) ? $value : ''; 61 | } 62 | 63 | /** 64 | * Writes the session data to the storage. 65 | * @see http://php.net/sessionhandlerinterface.write 66 | * @param string $sessionId Session ID , see http://php.net/function.session-id 67 | * @param string $data Serialized session data to save 68 | * @return bool true on success, false on failure 69 | */ 70 | public function write($sessionId, $data) 71 | { 72 | $sessionId = $this->getSessionId($sessionId); 73 | $value = $this->memcached->get($sessionId); 74 | if (empty($value)) { 75 | return $this->memcached->add($sessionId, $data, $this->sessionExpiry); 76 | } else { 77 | return $this->memcached->replace($sessionId, $data, $this->sessionExpiry); 78 | } 79 | } 80 | 81 | /** 82 | * Destroys a session. 83 | * @see http://php.net/sessionhandlerinterface.destroy 84 | * @param string $sessionId Session ID, see http://php.net/function.session-id 85 | * @return bool true on success, false on failure 86 | */ 87 | public function destroy($sessionId) 88 | { 89 | $sessionId = $this->getSessionId($sessionId); 90 | 91 | return $this->memcached->delete($sessionId); 92 | } 93 | 94 | /** 95 | * Cleans up expired sessions (garbage collection). 96 | * @see http://php.net/sessionhandlerinterface.gc 97 | * @param string|int $maxlifetime Sessions that have not updated for the last maxlifetime seconds will be removed 98 | * @return bool true on success, false on failure 99 | */ 100 | public function gc($maxlifetime) 101 | { 102 | //memcached automatically expires 103 | return true; 104 | } 105 | 106 | private function getSessionId($sessionId) 107 | { 108 | return $this->sessionPrefix . '_' . $sessionId; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/ElasticacheSessionHandlerTest.php: -------------------------------------------------------------------------------- 1 | mockMemcached = m::mock('memcachedClass'); 17 | $mockConfig = m::mock('Config'); 18 | 19 | $mockConfig->shouldReceive('get') 20 | ->with('session.lifetime') 21 | ->andReturn(10); 22 | 23 | $mockConfig->shouldReceive('get') 24 | ->with('session.cookie') 25 | ->andReturn('test_session_prefix'); 26 | 27 | $this->mockApp->shouldReceive('make') 28 | ->with('config') 29 | ->andReturn($mockConfig); 30 | 31 | $this->sessionHandler = 32 | new ElasticacheSessionHandler($this->mockMemcached, $this->mockApp); 33 | } 34 | 35 | public function testSessionExpiryConvertedToSeconds() 36 | { 37 | $expiry = $this->sessionHandler->sessionExpiry; 38 | $this->assertEquals(600, $expiry); 39 | } 40 | 41 | public function testOpenReturnsTrueIfMemcachedExists() 42 | { 43 | $result = $this->sessionHandler->open('test_path', 'test_session'); 44 | $this->assertTrue($result); 45 | } 46 | 47 | public function testOpenReturnsFalseIfMemcachedIsNull() 48 | { 49 | $handler = new ElasticacheSessionHandler(null, $this->mockApp); 50 | $result = $handler->open('test_path', 'test_session'); 51 | $this->assertFalse($result); 52 | } 53 | 54 | public function testCloseReturnsTrue() 55 | { 56 | $result = $this->sessionHandler->close(); 57 | $this->assertTrue($result); 58 | } 59 | 60 | public function testGcReturnsTrue() 61 | { 62 | $result = $this->sessionHandler->gc(m::anyOf('int')); 63 | $this->assertTrue($result); 64 | } 65 | 66 | public function testReadReturnsValueIfPresent() 67 | { 68 | $this->setGetExpectations('test_id', 'test_value'); 69 | $value = $this->sessionHandler->read('test_id'); 70 | $this->assertEquals('test_value', $value); 71 | } 72 | 73 | public function testReadReturnsEmptyStringIfNotPresent() 74 | { 75 | $this->setGetExpectations('test_id', false); 76 | $value = $this->sessionHandler->read('test_id'); 77 | $this->assertEquals('', $value); 78 | } 79 | 80 | public function testWriteSessionCallsAddIfNotPresent() 81 | { 82 | $this->setGetExpectations('test_id', false); 83 | $this->mockMemcached->shouldReceive('add') 84 | ->once()->andReturn(true); 85 | $this->mockMemcached->shouldReceive('replace') 86 | ->never(); 87 | 88 | $result = $this->sessionHandler->write('test_id', 'test_data'); 89 | $this->assertTrue($result); 90 | } 91 | 92 | public function testWriteSessionCallsReplaceIfPresent() 93 | { 94 | $this->setGetExpectations('test_id', true); 95 | $this->mockMemcached->shouldReceive('replace') 96 | ->once()->andReturn(true); 97 | $this->mockMemcached->shouldReceive('add') 98 | ->never(); 99 | 100 | $result = $this->sessionHandler->write('test_id', 'test_data'); 101 | $this->assertTrue($result); 102 | } 103 | 104 | public function testDestroy() 105 | { 106 | $this->mockMemcached->shouldReceive('delete') 107 | ->once()->with('test_session_prefix_test_id') 108 | ->andReturn(true); 109 | 110 | $result = $this->sessionHandler->destroy('test_id'); 111 | $this->assertTrue($result); 112 | } 113 | 114 | private function setGetExpectations($id, $value) 115 | { 116 | $this->mockMemcached->shouldReceive('get') 117 | ->once() 118 | ->with('test_session_prefix_' . $id) 119 | ->andReturn($value); 120 | } 121 | 122 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | mockApp = m::mock('Illuminate\Contracts\Foundation\Application'); 13 | } 14 | 15 | public function tearDown() 16 | { 17 | m::close(); 18 | } 19 | 20 | 21 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |