diff --git a/.travis.yml b/.travis.yml index 964a4d5..cb3980c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,10 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + +services: + - redis-server env: global: @@ -30,6 +34,8 @@ matrix: - FOC_VALIDATE=1 before_script: + - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - phpenv rehash - git clone -b master https://github.com/FriendsOfCake/travis.git --depth 1 ../travis - ../travis/before_script.sh @@ -41,3 +47,6 @@ after_success: notifications: email: false + hipchat: + rooms: + secure: BgtN4zIM+HqmIIvGb5aJz3vuu2ACF5k/1iJb32B/BSZE7hQxJ9iSsnY0naZzv81cmFVXmndzp4I4dM8wLrISScqSz3iM7NNVxMSm+FVQt63DckikO39Slyh8HfSyY3pxuIzPuecuIs85Li1z2KnBue34QRyMqmaAYS+sdTXZONM= diff --git a/Model/Datasource/RedisSource.php b/Model/Datasource/RedisSource.php new file mode 100644 index 0000000..4af812b --- /dev/null +++ b/Model/Datasource/RedisSource.php @@ -0,0 +1,264 @@ + '127.0.0.1', + 'port' => 6379, + 'password' => '', + 'database' => 0, + 'timeout' => 0, + 'persistent' => false, + 'unix_socket' => '', + 'prefix' => '', + ); + +/** + * Configuration. + * + * @var array + */ + public $config = array(); + +/** + * A reference to the physical connection of this DataSource. + * + * @var Redis + */ + protected $_connection = null; + +/** + * Whether or not we are connected to the DataSource. + * + * @var bool + */ + public $connected = false; + +/** + * Whether or not source data like available tables and schema descriptions should be cached. + * + * @var bool + */ + public $cacheSources = false; + +/** + * Constructor. + * + * @param array $config Array of configuration information for the Datasource + * @return bool True if connecting to the DataSource succeeds, else false + */ + public function __construct($config = array()) { + parent::__construct($config); + + if (!$this->enabled()) { + return false; + } + + $this->_connection = new Redis(); + + return $this->connect(); + } + +/** + * Destructor. + * + * Closes the connection to the host (if needed). + * + * @return void + * @todo Write test + */ + public function __destruct() { + if (!$this->config['persistent']) { + $this->close(); + } + } + +/** + * Passes (non-existing) method calls to `Redis`. + * + * @param string $name The name of the method being called + * @param array $arguments An enumerated array containing the parameters passed to the method + * @return mixed Method return value + * @todo Throw exception(s) + */ + public function __call($name, $arguments) { + if (!method_exists($this->_connection, $name)) { + return false; + } + + return call_user_func_array(array($this->_connection, $name), $arguments); + } + +/** + * Check that the redis extension is loaded. + * + * @return bool Whether or not the extension is loaded + */ + public function enabled() { + return extension_loaded('redis'); + } + +/** + * Connects to the database using options in the given configuration array. + * + * "Connects mean: + * - connect + * - authenticate + * - select + * - setPrefix + * + * @return bool + */ + public function connect() { + $this->connected = $this->_connect(); + $this->connected = $this->connected && $this->_authenticate(); + $this->connected = $this->connected && $this->_select(); + $this->connected = $this->connected && $this->_setPrefix(); + + return $this->connected; + } + +/** + * Connects to the database using options in the given configuration array. + * + * @return bool True if connecting to the DataSource succeeds, else false + */ + protected function _connect() { + // TODO: Remove useless try / catch? + try { + if ($this->config['unix_socket']) { + return $this->_connection->connect($this->config['unix_socket']); + } elseif (!$this->config['persistent']) { + return $this->_connection->connect( + $this->config['host'], $this->config['port'], $this->config['timeout'] + ); + } else { + $persistentId = crc32(serialize($this->config)); + + return $this->_connection->pconnect( + $this->config['host'], $this->config['port'], $this->config['timeout'], $persistentId + ); + } + } catch (RedisException $e) { + return false; + } + } + +/** + * Authenticates to the database (if needed) using options in the given configuration array. + * + * @return bool True if the authentication succeeded or no password was specified, else false + */ + protected function _authenticate() { + if ($this->config['password']) { + return $this->_connection->auth($this->config['password']); + } + + return true; + } + +/** + * Selects a database (if needed) using options in the given configuration array. + * + * @return bool True if the select succeeded or no database was specified, else false + */ + protected function _select() { + if ($this->config['database']) { + return $this->_connection->select($this->config['database']); + } + + return true; + } + +/** + * Sets a prefix for all keys (if needed) using options in the given configuration array. + * + * @return bool True if setting the prefix succeeded or no prefix was specified, else false + */ + protected function _setPrefix() { + if ($this->config['prefix']) { + return $this->_connection->setOption(Redis::OPT_PREFIX, $this->config['prefix']); + } + + return true; + } + +/** + * Closes a connection. + * + * @return bool Always true + */ + public function close() { + // TODO: Remove useless condition + if ($this->isConnected()) { + $this->_connection->close(); + } + + $this->connected = false; + $this->_connection = null; + + return true; + } + +/** + * Checks if the source is connected to the database. + * + * @return bool True if the database is connected, else false + */ + public function isConnected() { + return $this->connected; + } + +/** + * Caches/returns cached results for child instances. + * + * @param mixed $data List of tables + * @return array Array of sources available in this datasource + * @todo: Remove useless method? + */ + public function listSources($data = null) { + return parent::listSources($data); + } + +/** + * Returns a Model description (metadata) or null if none found. + * + * @param Model|string $model Name of database table to inspect or model instance + * @return array Array of Metadata for the $model + * @todo: Remove useless method? + */ + public function describe($model) { + return parent::describe($model); + } + +/** + * Returns an SQL calculation, i.e. COUNT() or MAX() + * + * @param Model $Model The model to get a calculated field for + * @param string $func Lowercase name of SQL function, i.e. 'count' or 'max' + * @param array $params Function parameters (any values must be quoted manually) + * @return string An SQL calculation function + * @todo Remove useless method? + */ + public function calculate(Model $Model, $func, $params = array()) { + return array('count' => true); + } + +} diff --git a/Model/Datasource/empty b/Model/Datasource/empty deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index a41356b..765a4fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Redis (DataSource) plugin for CakePHP -[![Build Status](https://travis-ci.org/Oefenweb/cakephp-redis.png?branch=master)](https://travis-ci.org/Oefenweb/cakephp-redis) [![Coverage Status](https://coveralls.io/repos/Oefenweb/cakephp-redis/badge.png)](https://coveralls.io/r/Oefenweb/cakephp-redis) +[![Build Status](https://travis-ci.org/Oefenweb/cakephp-redis.png?branch=master)](https://travis-ci.org/Oefenweb/cakephp-redis) [![Coverage Status](https://coveralls.io/repos/Oefenweb/cakephp-redis/badge.png)](https://coveralls.io/r/Oefenweb/cakephp-redis) [![Packagist downloads](http://img.shields.io/packagist/dt/Oefenweb/cakephp-redis.svg)](https://packagist.org/packages/oefenweb/cakephp-redis) The Redis (DataSource) plugin ... diff --git a/Test/Case/Model/Datasource/RedisSourceTest.php b/Test/Case/Model/Datasource/RedisSourceTest.php new file mode 100644 index 0000000..991480c --- /dev/null +++ b/Test/Case/Model/Datasource/RedisSourceTest.php @@ -0,0 +1,614 @@ +getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + // Set expectations for constructor calls + $Source->expects($this->once())->method('enabled')->will($this->returnValue(false)); + $Source->expects($this->never())->method('connect'); + + // Now call the constructor + $reflectedClass = new ReflectionClass('TestRedisSource'); + $constructor = $reflectedClass->getConstructor(); + $constructor->invoke($Source); + } + +/** + * testConstructExtensionLoaded method + * + * Tests that `connect` will be called when redis extension is loaded. + * + * @return void + */ + public function testConstructExtensionLoaded() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + // Set expectations for constructor calls + $Source->expects($this->once())->method('enabled')->will($this->returnValue(true)); + $Source->expects($this->once())->method('connect'); + + // Now call the constructor + $reflectedClass = new ReflectionClass('TestRedisSource'); + $constructor = $reflectedClass->getConstructor(); + $constructor->invoke($Source); + + $expected = 'Redis'; + $result = $Source->_connection; + + $this->assertInstanceOf($expected, $result); + } + +/** + * testConnected method + * + * @return void + */ + public function testIsConnected() { + $Source = new TestRedisSource(); + + $Source->connected = true; + $result = $Source->isConnected(); + + $this->assertTrue($result); + + $Source->connected = false; + $result = $Source->isConnected(); + + $this->assertFalse($result); + } + +/** + * testConnectException method + * + * @return void + */ + public function testConnectException() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $unixSocket = '/foo/bar'; + + $Source->config = array('unix_socket' => $unixSocket); + $Source->_connection = $this->getMock('Redis', array('connect')); + + // Set expectations for connect calls + $Source->_connection->expects($this->once())->method('connect') + ->with($this->equalTo($unixSocket))->will($this->throwException(new RedisException)); + + // Now call _connect + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_connect'); + $result = $connect->invoke($Source); + + $this->assertFalse($result); + } + +/** + * testConnectUnixSocket method + * + * @return void + */ + public function testConnectUnixSocket() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $unixSocket = '/foo/bar'; + + $Source->config = array('unix_socket' => $unixSocket); + $Source->_connection = $this->getMock('Redis', array('connect')); + + // Set expectations for connect calls + $Source->_connection->expects($this->once())->method('connect') + ->with($this->equalTo($unixSocket))->will($this->returnValue(true)); + + // Now call _connect + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_connect'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testConnectTcp method + * + * @return void + */ + public function testConnectTcp() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $unixSocket = ''; + $persistent = false; + $host = 'foo'; + $port = 'bar'; + $timeout = 0; + + $Source->config = array( + 'unix_socket' => $unixSocket, + 'persistent' => $persistent, + 'host' => $host, + 'port' => $port, + 'timeout' => $timeout, + ); + $Source->_connection = $this->getMock('Redis', array('connect')); + + // Set expectations for connect calls + $Source->_connection->expects($this->once())->method('connect') + ->with($this->equalTo($host), $this->equalTo($port), $this->equalTo($timeout)) + ->will($this->returnValue(true)); + + // Now call _connect + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_connect'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testConnectTcpPersistent method + * + * @return void + */ + public function testConnectTcpPersistent() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $unixSocket = ''; + $persistent = true; + $host = 'foo'; + $port = 'bar'; + $timeout = 0; + + $Source->config = array( + 'unix_socket' => $unixSocket, + 'persistent' => $persistent, + 'host' => $host, + 'port' => $port, + 'timeout' => $timeout, + ); + $Source->_connection = $this->getMock('Redis', array('pconnect')); + + // Set expectations for pconnect calls + $Source->_connection->expects($this->once())->method('pconnect') + ->with($this->equalTo($host), $this->equalTo($port), $this->equalTo($timeout), $this->anything()) + ->will($this->returnValue(true)); + + // Now call _connect + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_connect'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testCall method + * + * @return void + */ + public function testCall() { + $Source = new TestRedisSource(); + + // + // Existing method + // + + $result = $Source->ping(); + $expected = '+PONG'; + + $this->assertIdentical($result, $expected); + + // + // Non-existing method + // + + $result = $Source->pang(); + + $this->assertFalse($result); + } + +/** + * testNoAuthenticate method + * + * @return void + */ + public function testNoAuthenticate() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $password = ''; + + $Source->config = array('password' => $password); + $Source->_connection = $this->getMock('Redis', array('auth')); + + // Set expectations for constructor calls + $Source->_connection->expects($this->never())->method('auth'); + + // Now call _authenticate + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_authenticate'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testSuccessfulAuthenticate method + * + * @return void + */ + public function testSuccessfulAuthenticate() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $password = 'foo'; + + $Source->config = array('password' => $password); + $Source->_connection = $this->getMock('Redis', array('auth')); + + // Set expectations for auth calls + $Source->_connection->expects($this->once())->method('auth') + ->with($this->equalTo($password))->will($this->returnValue(true)); + + // Now call _authenticate + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_authenticate'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testFailingAuthenticate method + * + * @return void + */ + public function testFailingAuthenticate() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $password = 'foo'; + + $Source->config = array('password' => $password); + $Source->_connection = $this->getMock('Redis', array('auth')); + + // Set expectations for auth calls + $Source->_connection->expects($this->once())->method('auth') + ->with($this->equalTo($password))->will($this->returnValue(false)); + + // Now call _authenticate + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_authenticate'); + $result = $connect->invoke($Source); + + $this->assertFalse($result); + } + +/** + * testNoSelect method + * + * @return void + */ + public function testNoSelect() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $database = ''; + + $Source->config = array('database' => $database); + $Source->_connection = $this->getMock('Redis', array('select')); + + // Set expectations for select calls + $Source->_connection->expects($this->never())->method('select'); + + // Now call _Select + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_select'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testSuccessfulSelect method + * + * @return void + */ + public function testSuccessfulSelect() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $database = 'foo'; + + $Source->config = array('database' => $database); + $Source->_connection = $this->getMock('Redis', array('select')); + + // Set expectations for select calls + $Source->_connection->expects($this->once())->method('select') + ->with($this->equalTo($database))->will($this->returnValue(true)); + + // Now call _Select + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_select'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testFailingSelect method + * + * @return void + */ + public function testFailingSelect() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $database = 'foo'; + + $Source->config = array('database' => $database); + $Source->_connection = $this->getMock('Redis', array('select')); + + // Set expectations for select calls + $Source->_connection->expects($this->once())->method('select') + ->with($this->equalTo($database))->will($this->returnValue(false)); + + // Now call _Select + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_select'); + $result = $connect->invoke($Source); + + $this->assertFalse($result); + } + +/** + * testNoSelect method + * + * @return void + */ + public function testNoSetPrefix() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $prefix = ''; + + $Source->config = array('prefix' => $prefix); + $Source->_connection = $this->getMock('Redis', array('setOption')); + + // Set expectations for setOption calls + $Source->_connection->expects($this->never())->method('setOption'); + + // Now call _Select + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_setPrefix'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testSuccessfulSelect method + * + * @return void + */ + public function testSuccessfulSetPrefix() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $prefix = 'foo'; + + $Source->config = array('prefix' => $prefix); + $Source->_connection = $this->getMock('Redis', array('setOption')); + + // Set expectations for setOption calls + $Source->_connection->expects($this->once())->method('setOption') + ->with($this->equalTo(Redis::OPT_PREFIX), $this->equalTo($prefix))->will($this->returnValue(true)); + + // Now call _Select + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_setPrefix'); + $result = $connect->invoke($Source); + + $this->assertTrue($result); + } + +/** + * testFailingSelect method + * + * @return void + */ + public function testFailingSetPrefix() { + // Get mock, without the constructor being called + $Source = $this->getMockBuilder('TestRedisSource')->disableOriginalConstructor()->getMock(); + + $prefix = 'foo'; + + $Source->config = array('prefix' => $prefix); + $Source->_connection = $this->getMock('Redis', array('setOption')); + + // Set expectations for setOption calls + $Source->_connection->expects($this->once())->method('setOption') + ->with($this->equalTo(Redis::OPT_PREFIX), $this->equalTo($prefix))->will($this->returnValue(false)); + + // Now call _Select + $reflectedClass = new ReflectionClass('TestRedisSource'); + $connect = $reflectedClass->getMethod('_setPrefix'); + $result = $connect->invoke($Source); + + $this->assertFalse($result); + } + + public function testCloseNotConnected() { + $Source = new TestRedisSource(); + + $Source->connected = false; + $Source->_connection = $this->getMock('Redis', array('close')); + + // Set expectations for close calls + $Source->_connection->expects($this->never())->method('close'); + + $result = $Source->close(); + + $this->assertFalse($Source->connected); + $this->assertNull($Source->_connection); + $this->assertTrue($result); + } + + public function testCloseConnected() { + $Source = new TestRedisSource(); + + $Source->connected = true; + $Source->_connection = $this->getMock('Redis', array('close')); + + // Set expectations for close calls + $Source->_connection->expects($this->once())->method('close'); + + $result = $Source->close(); + + $this->assertFalse($Source->connected); + $this->assertNull($Source->_connection); + $this->assertTrue($result); + } + +/** + * testListSources method + * + * @return void + */ + public function testListSources() { + $Source = new TestRedisSource(); + + $result = $Source->listSources(); + + $this->assertNull($result); + } + +/** + * testDescribe method + * + * @return void + */ + public function testDescribe() { + $Source = new TestRedisSource(); + $Model = $this->getMockForModel('Model'); + + $result = $Source->describe($Model); + + $this->assertNull($result); + } + +/** + * testCalculate method + * + * @return void + */ + public function testCalculate() { + $Source = new TestRedisSource(); + $Model = $this->getMockForModel('Model'); + $func = 'foo'; + $params = array('b', 'a', 'r'); + + $result = $Source->calculate($Model, $func, $params); + $expected = array('count' => true); + + $this->assertIdentical($expected, $result); + } + +} diff --git a/Test/Case/Model/Datasource/empty b/Test/Case/Model/Datasource/empty deleted file mode 100644 index e69de29..0000000