├── .gitignore ├── composer.json ├── LICENSE ├── src ├── ORM │ ├── MongoQuery.php │ ├── ResultSet.php │ ├── Document.php │ ├── MongoFinder.php │ └── Table.php └── Database │ ├── Schema │ └── MongoSchema.php │ ├── Connection.php │ └── Driver │ └── Mongodb.php ├── README.md └── tests └── TestCase └── ORM └── MongoFinderTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hayko/mongodb", 3 | "description": "An Mongodb datasource for CakePHP 3.0", 4 | "type": "cakephp-plugin", 5 | "require": { 6 | "php": "^5.4|^7.0", 7 | "ext-mongodb": "^1.3", 8 | "cakephp/cakephp": "^3.5", 9 | "mongodb/mongodb": "^1.2" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^6.0" 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "tiaguinho", 18 | "email": "tiaguinhocab@gmail.com" 19 | } 20 | ], 21 | "autoload": { 22 | "psr-4": { 23 | "Hayko\\Mongodb\\": "src", 24 | "Hayko\\Mongodb\\Test\\": "tests" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tiago Temporin 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 | -------------------------------------------------------------------------------- /src/ORM/MongoQuery.php: -------------------------------------------------------------------------------- 1 | _results = $results; 34 | $this->_rows = $rows; 35 | } 36 | 37 | /** 38 | * return array with results 39 | * 40 | * @return array 41 | * @access public 42 | */ 43 | public function all() 44 | { 45 | return $this->_results; 46 | } 47 | 48 | /** 49 | * return number of rows 50 | * 51 | * @return int 52 | * @access public 53 | */ 54 | public function count() 55 | { 56 | return $this->_rows; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ORM/ResultSet.php: -------------------------------------------------------------------------------- 1 | _results = iterator_to_array($cursor); 34 | $this->_table = $table; 35 | } 36 | 37 | /** 38 | * convert mongo documents in cake entitys 39 | * 40 | * @return \Cake\ORM\Entity[] $results 41 | * @access public 42 | * @throws \Exception 43 | */ 44 | public function toArray() 45 | { 46 | $results = []; 47 | foreach ($this->_results as $result) { 48 | $document = new Document($result, $this->_table); 49 | $results[] = $document->cakefy(); 50 | } 51 | 52 | return $results; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Database/Schema/MongoSchema.php: -------------------------------------------------------------------------------- 1 | _connection = $conn; 29 | } 30 | 31 | /** 32 | * Describe 33 | * 34 | * @access public 35 | * @param string $name describe 36 | * @return TableSchema 37 | */ 38 | public function describe($name) 39 | { 40 | if (strpos($name, '.')) { 41 | list(, $name) = explode('.', $name); 42 | } 43 | 44 | $table = new TableSchema($name); 45 | 46 | if (empty($table->primaryKey())) { 47 | $table->addColumn('_id', ['type' => 'string', 'default' => new ObjectId(), 'null' => false]); 48 | $table->addConstraint('_id', ['type' => 'primary', 'columns' => ['_id']]); 49 | } 50 | 51 | return $table; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ORM/Document.php: -------------------------------------------------------------------------------- 1 | _document = $document; 38 | $this->_registryAlias = $table; 39 | } 40 | 41 | /** 42 | * convert mongo document into cake entity 43 | * 44 | * @return \Cake\ORM\Entity 45 | * @access public 46 | * @throws Exception 47 | */ 48 | public function cakefy() 49 | { 50 | $document = []; 51 | 52 | foreach ($this->_document as $field => $value) { 53 | $type = gettype($value); 54 | if ($type == 'object') { 55 | switch (get_class($value)) { 56 | case 'MongoDB\BSON\ObjectId': 57 | $document[$field] = $value->__toString(); 58 | break; 59 | 60 | case 'MongoDB\BSON\UTCDateTime': 61 | $document[$field] = $value->toDateTime(); 62 | break; 63 | 64 | default: 65 | if ($value instanceof \MongoDB\BSON\Serializable) { 66 | $document[$field] = $this->serializeObjects($value); 67 | } else { 68 | throw new Exception(get_class($value) . ' conversion not implemented.'); 69 | } 70 | } elseif ($type == 'array') { 71 | $document[$field] = $this->cakefy(); 72 | } else { 73 | $document[$field] = $value; 74 | } 75 | } 76 | 77 | $inflector = new \Cake\Utility\Inflector(); 78 | $entityName = '\\App\\Model\\Entity\\'.$inflector->singularize($this->_registryAlias); 79 | return new $entityName($document, ['markClean' => true, 'markNew' => false, 'source' => $this->_registryAlias]); 80 | } 81 | 82 | 83 | 84 | 85 | private function serializeObjects($obj){ 86 | if ($obj instanceof \MongoDB\BSON\Serializable) { 87 | foreach($obj as $field=> $value){ 88 | if ($value instanceof \MongoDB\BSON\Serializable) { 89 | $obj[$field] = $this->serializeObjects($value); 90 | } 91 | } 92 | return $obj->bsonSerialize(); 93 | }else{ 94 | return $obj; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Slack](https://img.shields.io/badge/join%20the%20conversation-on%20slack-green.svg)](https://mongodb-cakephp3.slack.com/messages/general/) 2 | 3 | Mongodb for Cakephp3 4 | ======== 5 | 6 | An Mongodb datasource for CakePHP 3.5 7 | 8 | ## Installing via composer 9 | 10 | Install [composer](http://getcomposer.org) and run: 11 | 12 | ```bash 13 | composer require hayko/mongodb dev-master 14 | ``` 15 | 16 | ## Connecting the Plugin to your application 17 | 18 | add the following line in your config/bootstrap.php to tell your application to load the plugin: 19 | 20 | ```php 21 | Plugin::load('Hayko/Mongodb'); 22 | 23 | ``` 24 | 25 | ## Defining a connection 26 | Now, you need to set the connection in your config/app.php file: 27 | 28 | ```php 29 | 'Datasources' => [ 30 | 'default' => [ 31 | 'className' => 'Hayko\Mongodb\Database\Connection', 32 | 'driver' => 'Hayko\Mongodb\Database\Driver\Mongodb', 33 | 'persistent' => false, 34 | 'host' => 'localhost', 35 | 'port' => 27017, 36 | 'login' => '', 37 | 'password' => '', 38 | 'database' => 'devmongo', 39 | 'ssh_host' => '', 40 | 'ssh_port' => 22, 41 | 'ssh_user' => '', 42 | 'ssh_password' => '', 43 | 'ssh_pubkey_path' => '', 44 | 'ssh_privatekey_path' => '', 45 | 'ssh_pubkey_passphrase' => '' 46 | ], 47 | ], 48 | ``` 49 | 50 | ### SSH tunnel variables (starting with 'ssh_') 51 | If you want to connect to MongoDB using a SSH tunnel, you need to set additional variables in your Datasource. Some variables are unnecessary, depending on how you intend to connect. IF you're connecting using a SSH key file, the ```ssh_pubkey_path``` and ```ssh_privatekey_path``` variables are necessary and the ```ssh_password``` variable is unnecessary. If you're connecting using a text-based password (which is **not** a wise idea), the reverse is true. The function needs, at minimum, ```ssh_host```, ```ssh_user``` and one method of authentication to establish a SSH tunnel. 52 | 53 | ## Models 54 | After that, you need to load Hayko\Mongodb\ORM\Table in your tables class: 55 | 56 | ```php 57 | //src/Model/Table/YourTable.php 58 | 59 | use Hayko\Mongodb\ORM\Table; 60 | 61 | class CategoriesTable extends Table { 62 | 63 | } 64 | ``` 65 | 66 | ## Observations 67 | 68 | The function find() works only in the old fashion way. 69 | So, if you want to find something, you to do like the example: 70 | 71 | ```php 72 | $this->Categories->find('all', ['conditions' => ['name' => 'teste']]); 73 | $this->Categories->find('all', ['conditions' => ['name LIKE' => 'teste']]); 74 | $this->Categories->find('all', ['conditions' => ['name' => 'teste'], 'limit' => 3]); 75 | ``` 76 | 77 | You can also use the advanced conditions of MongoDB using the `MongoDB\BSON` namespace 78 | 79 | ```php 80 | $this->Categories->find('all', ['conditions' => [ 81 | '_id' => new \MongoDB\BSON\ObjectId('5a7861909db0b47d605c3865'), 82 | 'foo.bar' => new \MongoDB\BSON\Regex('^(foo|bar)?baz$', 'i') 83 | ]]); 84 | ``` 85 | 86 | ## LICENSE 87 | 88 | [The MIT License (MIT) Copyright (c) 2013](http://opensource.org/licenses/MIT) 89 | -------------------------------------------------------------------------------- /src/Database/Connection.php: -------------------------------------------------------------------------------- 1 | _driver->connected) { 43 | $this->_driver->disconnect(); 44 | unset($this->_driver); 45 | } 46 | } 47 | 48 | /** 49 | * return configuration 50 | * 51 | * @return array $_config 52 | * @access public 53 | */ 54 | public function config() 55 | { 56 | return $this->_config; 57 | } 58 | 59 | /** 60 | * return configuration name 61 | * 62 | * @return string 63 | * @access public 64 | */ 65 | public function configName() 66 | { 67 | return 'mongodb'; 68 | } 69 | 70 | /** 71 | * @param null $driver driver 72 | * @param array $config configuration 73 | * @return Haykodb|resource 74 | */ 75 | public function driver($driver = null, $config = []) 76 | { 77 | if ($driver === null) { 78 | return $this->_driver; 79 | } 80 | $this->_driver = new Haykodb($config); 81 | 82 | return $this->_driver; 83 | } 84 | 85 | /** 86 | * connect to the database 87 | * 88 | * @return bool 89 | * @access public 90 | */ 91 | public function connect() 92 | { 93 | try { 94 | $this->_driver->connect(); 95 | 96 | return true; 97 | } catch (\Exception $e) { 98 | throw new MissingConnectionException(['reason' => $e->getMessage()]); 99 | } 100 | } 101 | 102 | /** 103 | * disconnect from the database 104 | * 105 | * @return boole 106 | * @access public 107 | */ 108 | public function disconnect() 109 | { 110 | if ($this->_driver->isConnected()) { 111 | return $this->_driver->disconnect(); 112 | } 113 | 114 | return true; 115 | } 116 | 117 | /** 118 | * database connection status 119 | * 120 | * @return bool 121 | * @access public 122 | */ 123 | public function isConnected() 124 | { 125 | return $this->_driver->isConnected(); 126 | } 127 | 128 | /** 129 | * Gets a Schema\Collection object for this connection. 130 | * 131 | * @return MongoSchema 132 | */ 133 | public function getSchemaCollection() 134 | { 135 | if ($this->_schemaCollection !== null) { 136 | return $this->_schemaCollection; 137 | } 138 | 139 | return $this->_schemaCollection = new MongoSchema($this->_driver); 140 | } 141 | 142 | /** 143 | * Mongo doesn't support transaction 144 | * 145 | * @param callable $transaction 146 | * @return false 147 | * @access public 148 | */ 149 | public function transactional(callable $transaction) 150 | { 151 | return false; 152 | } 153 | 154 | /** 155 | * Mongo doesn't support foreign keys 156 | * 157 | * @param callable $operation 158 | * @return false 159 | * @access public 160 | */ 161 | public function disableConstraints(callable $operation) 162 | { 163 | return false; 164 | } 165 | 166 | /** 167 | * @param null $table 168 | * @param null $column 169 | * @return int|string|void 170 | */ 171 | public function lastInsertId($table = null, $column = null) 172 | { 173 | // TODO: Implement lastInsertId() method. 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/TestCase/ORM/MongoFinderTest.php: -------------------------------------------------------------------------------- 1 | setTable('tests'); 18 | $this->setEntityClass('Hayko\Mongodb\Test\TestCase\ORM\MongoTest'); 19 | $this->setConnection(ConnectionManager::get('mongodb_test', false)); 20 | parent::initialize($config); 21 | } 22 | } 23 | class MongoTest extends Entity 24 | { 25 | } 26 | 27 | class MongoFinderTest extends TestCase 28 | { 29 | /** 30 | * @var MongoTestsTable $table 31 | */ 32 | public $table; 33 | 34 | public function setUp() 35 | { 36 | parent::setUp(); 37 | Cache::disable(); 38 | $this->table = TableRegistry::getTableLocator()->get('MongoTests', ['className' => 'Hayko\Mongodb\Test\TestCase\ORM\MongoTestsTable']); 39 | } 40 | 41 | public function tearDown() 42 | { 43 | parent::tearDown(); 44 | $this->table->deleteAll([]); 45 | TableRegistry::getTableLocator()->clear(); 46 | } 47 | 48 | public function testFind() 49 | { 50 | $this->assertTrue($this->table instanceof Table); 51 | $data = ['foo' => 'bar', 'baz' => true]; 52 | $entity = $this->table->newEntity($data); 53 | $this->assertNotFalse($this->table->save($entity)); 54 | 55 | $this->assertNotEmpty($this->table->find('all')); 56 | 57 | $data = ['foo' => ['bar' => 'baz']]; 58 | $entity = $this->table->newEntity($data); 59 | $this->assertNotFalse($this->table->save($entity)); 60 | 61 | $condition = [ 62 | 'foo.bar' => 'baz' 63 | ]; 64 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 65 | 66 | $condition = [ 67 | 'foo LIKE' => 'b%r' 68 | ]; 69 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 70 | 71 | $condition = [ 72 | 'foo' => ['$regex' => 'b.*r'] 73 | ]; 74 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 75 | 76 | $condition = [ 77 | 'foo.bar' => new Regex('^b.*z$', 'i') 78 | ]; 79 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 80 | 81 | $condition = [ 82 | 'OR' => [ 83 | ['foo' => 'bar'], 84 | ['foo' => 'baz'], 85 | ] 86 | ]; 87 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 88 | 89 | $condition = [ 90 | 'AND' => [ 91 | ['foo' => 'bar'], 92 | ['baz' => true], 93 | ] 94 | ]; 95 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 96 | 97 | 98 | $data = ['foo' => 125]; 99 | $entity = $this->table->newEntity($data); 100 | $this->assertNotFalse($this->table->save($entity)); 101 | 102 | $condition = [ 103 | 'foo >=' => 100 104 | ]; 105 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 106 | 107 | 108 | $data = ['foo' => 125, 'bar' => 150]; 109 | $entity = $this->table->newEntity($data); 110 | $this->assertNotFalse($this->table->save($entity)); 111 | 112 | $condition = [ 113 | 'foo < bar' 114 | ]; 115 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 116 | 117 | $condition = [ 118 | '$where' => "this.foo < this.bar" 119 | ]; 120 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 121 | 122 | $condition = [ 123 | ['foo' => 'bar'], 124 | [['baz' => true], ['foo LIKE' => 'b%']] 125 | ]; 126 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 127 | 128 | $condition = [ 129 | ['foo IN' => ['bar', 'baz']], 130 | ]; 131 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 132 | 133 | $condition = [ 134 | ['foo NOT IN' => ['bar', 'baz']], 135 | ]; 136 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 137 | 138 | $entity = $this->table->find('all')[0]; 139 | $condition = [ 140 | '_id' => $entity->get('_id') 141 | ]; 142 | $this->assertNotEmpty($this->table->find('all', ['where' => $condition])); 143 | 144 | $results = $this->table->find('all', ['limit' => 2, 'page' => 2]); 145 | $this->assertEquals(2, count($results)); 146 | $this->assertEquals(150, $results[1]->get('bar')); 147 | } 148 | 149 | public function testFindFirst() 150 | { 151 | $data = ['foo' => 'zip', 'baz' => true]; 152 | $entity = $this->table->newEntity($data); 153 | $this->assertNotFalse($this->table->save($entity)); 154 | $this->assertNotEmpty($this->table->find('first')); 155 | 156 | $data = ['foo' => 'bar', 'baz' => false]; 157 | $entity = $this->table->newEntity($data); 158 | $this->assertNotFalse($this->table->save($entity)); 159 | 160 | $expected = 'bar'; 161 | $this->assertEquals($expected, $this->table->find('first', ['order' => 'foo'])->get('foo')); 162 | 163 | $expected = 'zip'; 164 | $this->assertEquals($expected, $this->table->find('first', ['order' => ['foo' => 'desc']])->get('foo')); 165 | } 166 | 167 | public function testFindList() 168 | { 169 | $entity1 = $this->table->save($this->table->newEntity(['name' => 'foo', 'baz' => true])); 170 | $entity2 = $this->table->save($this->table->newEntity(['name' => 'bar', 'baz' => false])); 171 | $expected = [ 172 | (string)$entity1->get('_id') => 'foo', 173 | (string)$entity2->get('_id') => 'bar', 174 | ]; 175 | $this->assertEquals($expected, $this->table->find('list')); 176 | } 177 | 178 | public function testUpdate() 179 | { 180 | $entity1 = $this->table->save($this->table->newEntity(['name' => 'foo', 'baz' => true])); 181 | $this->table->save($this->table->newEntity(['name' => 'bar', 'baz' => false])); 182 | $this->table->save($this->table->newEntity(['name' => 'baz', 'baz' => false])); 183 | $entity1->set('name', 'biz'); 184 | $this->assertTrue((bool)$this->table->save($entity1)); 185 | 186 | $expected = 'biz'; 187 | $this->assertEquals($expected, $this->table->find('first', ['where' => ['baz' => true]])->get('name')); 188 | 189 | $this->assertEquals(2, $this->table->updateAll(['name' => 'foo'], ['baz' => false])); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Database/Driver/Mongodb.php: -------------------------------------------------------------------------------- 1 | true, 61 | 'persistent' => true, 62 | 'host' => 'localhost', 63 | 'database' => '', 64 | 'port' => 27017, 65 | 'login' => '', 66 | 'password' => '', 67 | 'replicaset' => '', 68 | ]; 69 | 70 | /** 71 | * Direct connection with database 72 | * 73 | * @var mixed null | Mongo 74 | * @access private 75 | */ 76 | private $connection = null; 77 | 78 | /** 79 | * @param array $config configuration 80 | */ 81 | public function __construct($config) 82 | { 83 | $this->_config = $config; 84 | } 85 | 86 | /** 87 | * return configuration 88 | * 89 | * @return array 90 | * @access public 91 | */ 92 | public function config() 93 | { 94 | return $this->_config; 95 | } 96 | 97 | /** 98 | * connect to the database 99 | * 100 | * @return bool 101 | * @access public 102 | */ 103 | public function connect() 104 | { 105 | try { 106 | if (($this->_config['ssh_user'] != '') && ($this->_config['ssh_host'])) { // Because a user is required for all of the SSH authentication functions. 107 | if (intval($this->_config['ssh_port']) != 0) { 108 | $port = $this->_config['ssh_port']; 109 | } else { 110 | $port = 22; // The default SSH port. 111 | } 112 | $spongebob = ssh2_connect($this->_config['ssh_host'], $port); 113 | if (!$spongebob) { 114 | trigger_error( 115 | 'Unable to establish a SSH connection to the host at ' . 116 | $this->_config['ssh_host'] . ':' . $port 117 | ); 118 | } 119 | if (($this->_config['ssh_pubkey_path'] != null) && 120 | ($this->_config['ssh_privatekey_path'] != null) 121 | ) { 122 | if ($this->_config['ssh_pubkey_passphrase'] != null) { 123 | if (!ssh2_auth_pubkey_file( 124 | $spongebob, 125 | $this->_config['ssh_user'], 126 | $this->_config['ssh_pubkey_path'], 127 | $this->_config['ssh_privatekey_path'], 128 | $this->_config['ssh_pubkey_passphrase'] 129 | )) { 130 | trigger_error( 131 | 'Unable to connect using the public keys specified at ' . 132 | $this->_config['ssh_pubkey_path'] . ' (for the public key), ' . 133 | $this->_config['ssh_privatekey_path'] . ' (for the private key) on ' . 134 | $this->_config['ssh_user'] . '@' . $this->_config['ssh_host'] . ':' . $port . 135 | ' (Using a passphrase to decrypt the key)' 136 | ); 137 | 138 | return false; 139 | } 140 | } else { 141 | if (ssh2_auth_pubkey_file( 142 | $spongebob, 143 | $this->_config['ssh_user'], 144 | $this->_config['ssh_pubkey_path'], 145 | $this->_config['ssh_privatekey_path'] 146 | )) { 147 | trigger_error( 148 | 'Unable to connect using the public keys specified at ' . 149 | $this->_config['ssh_pubkey_path'] . ' (for the public key), ' . 150 | $this->_config['ssh_privatekey_path'] . ' (for the private key) on ' . 151 | $this->_config['ssh_user'] . '@' . $this->_config['ssh_host'] . ':' . 152 | $port . ' (Not using a passphrase to decrypt the key)' 153 | ); 154 | 155 | return false; 156 | } 157 | } 158 | } elseif ($this->_config['ssh_password'] != '') { // While some people *could* have blank passwords, it's a really stupid idea. 159 | if (!ssh2_auth_password($spongebob, $this->_config['ssh_user'], $this->_config['ssh_password'])) { 160 | trigger_error( 161 | 'Unable to connect using the username and password combination for ' . 162 | $this->_config['ssh_user'] . '@' . $this->_config['ssh_host'] . ':' . $port 163 | ); 164 | 165 | return false; 166 | } 167 | } else { 168 | trigger_error('Neither a password or paths to public & private keys were specified in the configuration.'); 169 | 170 | return false; 171 | } 172 | 173 | $tunnel = ssh2_tunnel($spongebob, $this->_config['host'], $this->_config['port']); 174 | if (!$tunnel) { 175 | trigger_error( 176 | 'A SSH tunnel was unable to be created to access ' . 177 | $this->_config['host'] . ':' . $this->_config['port'] . ' on ' . 178 | $this->_config['ssh_user'] . '@' . $this->_config['ssh_host'] . ':' . $port 179 | ); 180 | } 181 | } 182 | 183 | $host = $this->createConnectionName(); 184 | 185 | if (version_compare($this->_driverVersion, '1.3.0', '<')) { 186 | throw new Exception(__("Please update your MongoDB PHP Driver ({0} < {1})", $this->_driverVersion, '1.3.0')); 187 | } 188 | 189 | if (isset($this->_config['replicaset']) && count($this->_config['replicaset']) === 2) { 190 | $this->connection = new \MongoDB\Client($this->_config['replicaset']['host'], $this->_config['replicaset']['options']); 191 | } else { 192 | $this->connection = new \MongoDB\Client($host); 193 | } 194 | 195 | if (isset($this->_config['slaveok'])) { 196 | $this->connection->getManager()->selectServer( 197 | new ReadPreference( 198 | $this->_config['slaveok'] 199 | ? ReadPreference::RP_SECONDARY_PREFERRED 200 | : ReadPreference::RP_PRIMARY 201 | ) 202 | ); 203 | } 204 | 205 | if ($this->_db = $this->connection->selectDatabase($this->_config['database'])) { 206 | $this->connected = true; 207 | } 208 | } catch (Exception $e) { 209 | trigger_error($e->getMessage()); 210 | } 211 | 212 | return $this->connected; 213 | } 214 | 215 | /** 216 | * create connection string 217 | * 218 | * @access private 219 | * @return string 220 | */ 221 | private function createConnectionName() 222 | { 223 | $host = ''; 224 | 225 | if ($this->_driverVersion >= '1.0.2') { 226 | $host = 'mongodb://'; 227 | } 228 | $hostname = $this->_config['host'] . ':' . $this->_config['port']; 229 | 230 | if (!empty($this->_config['login'])) { 231 | $host .= $this->_config['login'] . ':' . $this->_config['password'] . '@' . $hostname . '/' . $this->_config['database']; 232 | } else { 233 | $host .= $hostname; 234 | } 235 | 236 | return $host; 237 | } 238 | 239 | /** 240 | * return MongoCollection object 241 | * 242 | * @param string $collectionName name of collecion 243 | * @return \MongoDB\Collection|bool 244 | * @access public 245 | */ 246 | public function getCollection($collectionName = '') 247 | { 248 | if (!empty($collectionName)) { 249 | if (!$this->isConnected()) { 250 | $this->connect(); 251 | } 252 | 253 | $manager = new Manager($this->createConnectionName()); 254 | 255 | return new Collection($manager, $this->_config['database'], $collectionName); 256 | } 257 | 258 | return false; 259 | } 260 | 261 | /** 262 | * disconnect from the database 263 | * 264 | * @return bool 265 | * @access public 266 | */ 267 | public function disconnect() 268 | { 269 | if ($this->connected) { 270 | unset($this->_db, $this->connection); 271 | 272 | return !$this->connected; 273 | } 274 | 275 | return true; 276 | } 277 | 278 | /** 279 | * database connection status 280 | * 281 | * @return bool 282 | * @access public 283 | */ 284 | public function isConnected() 285 | { 286 | return $this->connected; 287 | } 288 | 289 | /** 290 | * @return bool 291 | */ 292 | public function enabled() 293 | { 294 | return true; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/ORM/MongoFinder.php: -------------------------------------------------------------------------------- 1 | [], 30 | 'where' => [], 31 | ]; 32 | 33 | /** 34 | * total number of rows 35 | * 36 | * @var int $_totalRows 37 | * @access protected 38 | */ 39 | protected $_totalRows; 40 | 41 | /** 42 | * set connection and options to find 43 | * 44 | * @param Collection $connection 45 | * @param array $options 46 | * @access public 47 | */ 48 | public function __construct($connection, $options = []) 49 | { 50 | $this->connection($connection); 51 | $this->_options = array_merge_recursive($this->_options, $options); 52 | 53 | if (isset($options['conditions']) && !empty($options['conditions'])) { 54 | $this->_options['where'] += $options['conditions']; 55 | unset($this->_options['conditions']); 56 | } 57 | 58 | if (!empty($this->_options['where'])) { 59 | $this->__translateNestedArray($this->_options['where']); 60 | $this->__translateConditions($this->_options['where']); 61 | } 62 | } 63 | 64 | /** 65 | * Convert ['foo' => 'bar', ['baz' => true]] 66 | * to 67 | * ['$and' => [['foo', 'bar'], ['$and' => ['baz' => true]]] 68 | * @param $conditions 69 | */ 70 | private function __translateNestedArray(&$conditions) 71 | { 72 | $and = isset($conditions['$and']) ? (array)$conditions['$and'] : []; 73 | foreach ($conditions as $key => $value) { 74 | if (is_numeric($key) && is_array($value)) { 75 | unset($conditions[$key]); 76 | $and[] = $value; 77 | } elseif (is_array($value) && !in_array(strtoupper($key), ['OR', '$OR', 'AND', '$AND'])) { 78 | $this->__translateNestedArray($conditions[$key]); 79 | } 80 | } 81 | if (!empty($and)) { 82 | $conditions['$and'] = $and; 83 | foreach (array_keys($conditions['$and']) as $key) { 84 | $this->__translateNestedArray($conditions['$and'][$key]); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * connection 91 | * 92 | * @param Collection $connection 93 | * @return Collection 94 | * @access public 95 | */ 96 | public function connection($connection = null) 97 | { 98 | if ($connection === null) { 99 | return $this->_connection; 100 | } 101 | 102 | $this->_connection = $connection; 103 | } 104 | 105 | /** 106 | * convert sql conditions into mongodb conditions 107 | * 108 | * '!=' => '$ne', 109 | * '>' => '$gt', 110 | * '>=' => '$gte', 111 | * '<' => '$lt', 112 | * '<=' => '$lte', 113 | * 'IN' => '$in', 114 | * 'NOT' => '$not', 115 | * 'NOT IN' => '$nin' 116 | * 117 | * @param array $conditions 118 | * @access private 119 | * @return array 120 | */ 121 | private function __translateConditions(&$conditions) 122 | { 123 | $operators = '<|>|<=|>=|!=|=|<>|IN|LIKE'; 124 | foreach ($conditions as $key => $value) { 125 | if (is_numeric($key) && is_array($value)) { 126 | $this->__translateConditions($conditions[$key]); 127 | } elseif (preg_match("/^(.+) ($operators)$/", $key, $matches)) { 128 | list(, $field, $operator) = $matches; 129 | if (substr($field, -3) === 'NOT') { 130 | $field = substr($field, 0, strlen($field) - 4); 131 | $operator = 'NOT ' . $operator; 132 | } 133 | $operator = $this->__translateOperator(strtoupper($operator)); 134 | unset($conditions[$key]); 135 | if (substr($operator, -4) === 'LIKE') { 136 | $value = str_replace('%', '.*', $value); 137 | $value = str_replace('?', '.', $value); 138 | if ($operator === 'NOT LIKE') { 139 | $value = "(?!$value)"; 140 | } 141 | $operator = '$regex'; 142 | $value = new Regex("^$value$", "i"); 143 | } 144 | $conditions[$field][$operator] = $value; 145 | } elseif (preg_match('/^OR|AND$/i', $key, $match)) { 146 | $operator = '$' . strtolower($match[0]); 147 | unset($conditions[$key]); 148 | foreach ($value as $nestedKey => $nestedValue) { 149 | if (!is_array($nestedValue)) { 150 | $nestedValue = [$nestedKey => $nestedValue]; 151 | $conditions[$operator][$nestedKey] = $nestedValue; 152 | } else { 153 | $conditions[$operator][$nestedKey] = $nestedValue; 154 | } 155 | $this->__translateConditions($conditions[$operator][$nestedKey]); 156 | } 157 | } elseif (preg_match("/^(.+) (<|>|<=|>=|!=|=) (.+)$/", $key, $matches) 158 | || (is_string($value) && preg_match("/^(.+) (<|>|<=|>=|!=|=) (.+)$/", $value, $matches)) 159 | ) { 160 | unset($conditions[$key]); 161 | array_splice($matches, 0, 1); 162 | $conditions['$where'] = implode(' ', array_map(function ($v) { 163 | if (preg_match("/^[\w.]+$/", $v) 164 | && substr($v, 0, strlen('this')) !== 'this' 165 | ) { 166 | $v = "this.$v"; 167 | } 168 | 169 | return $v; 170 | }, $matches)); 171 | } elseif ($key === '_id' && is_string($value)) { 172 | $conditions[$key] = new \MongoDB\BSON\ObjectId($value); 173 | } 174 | } 175 | 176 | return $conditions; 177 | } 178 | 179 | /** 180 | * Convert logical operator to MongoDB Query Selectors 181 | * @param string $operator 182 | * @return string 183 | */ 184 | private function __translateOperator($operator) 185 | { 186 | switch ($operator) { 187 | case '<': 188 | return '$lt'; 189 | case '<=': 190 | return '$lte'; 191 | case '>': 192 | return '$gt'; 193 | case '>=': 194 | return '$gte'; 195 | case '=': 196 | return '$eq'; 197 | case '!=': 198 | case '<>': 199 | return '$ne'; 200 | case 'NOT IN': 201 | return '$nin'; 202 | case 'IN': 203 | return '$in'; 204 | default: 205 | return $operator; 206 | } 207 | } 208 | 209 | /** 210 | * try to find documents 211 | * 212 | * @param array $options 213 | * @return Cursor $cursor 214 | * @access public 215 | */ 216 | public function find(array $options = []) 217 | { 218 | $this->__sortOption($options); 219 | $this->__limitOption($options); 220 | $cursor = $this->connection()->find($this->_options['where'], $options); 221 | if (is_array($cursor) || $cursor instanceof Countable) { 222 | $this->_totalRows = count($cursor); 223 | } else { 224 | $this->_totalRows = 0; 225 | } 226 | 227 | return $cursor; 228 | } 229 | 230 | /** 231 | * return all documents 232 | * 233 | * @return Cursor 234 | * @access public 235 | */ 236 | public function findAll() 237 | { 238 | return $this->find(); 239 | } 240 | 241 | /** 242 | * return all documents 243 | * 244 | * @return array 245 | * @access public 246 | */ 247 | public function findList() 248 | { 249 | $results = []; 250 | $keyField = isset($this->_options['keyField']) 251 | ? $this->_options['keyField'] 252 | : '_id'; 253 | 254 | $valueField = isset($this->_options['valueField']) 255 | ? $this->_options['valueField'] 256 | : 'name'; 257 | 258 | $cursor = $this->find(['projection' => [$keyField => 1, $valueField => 1]]); 259 | foreach (iterator_to_array($cursor) as $value) { 260 | $key = (string)Hash::get((array)$value, $keyField, ''); 261 | if ($key) { 262 | $results[$key] = (string)Hash::get((array)$value, $valueField, ''); 263 | } 264 | } 265 | 266 | return $results; 267 | } 268 | 269 | /** 270 | * return all documents 271 | * 272 | * @param array $options 273 | * @return array|object 274 | * @access public 275 | */ 276 | public function findFirst(array $options = []) 277 | { 278 | $this->__sortOption($options); 279 | $result = $this->connection()->findOne($this->_options['where'], $options); 280 | $this->_totalRows = (int)((bool)$result); 281 | 282 | return $result; 283 | } 284 | 285 | /** 286 | * Append sort to options with $this->_options['order'] 287 | * @param array $options 288 | */ 289 | private function __sortOption(array &$options) 290 | { 291 | if (!empty($this->_options['order'])) { 292 | $options['sort'] = array_map( 293 | function ($v) { 294 | return strtolower((string)$v) === 'desc' ? -1 : 1; 295 | }, 296 | Hash::get($options, 'sort', []) 297 | + Hash::normalize((array)$this->_options['order']) 298 | ); 299 | } 300 | } 301 | 302 | /** 303 | * Append limit and skip options 304 | * @param array $options 305 | */ 306 | private function __limitOption(array &$options) 307 | { 308 | if (!empty($this->_options['limit']) && !isset($options['limit'])) { 309 | $options['limit'] = $this->_options['limit']; 310 | } 311 | if (!empty($this->_options['page']) && $this->_options['page'] > 1 312 | && !empty($options['limit']) 313 | && !isset($options['skip']) 314 | ) { 315 | $options['skip'] = $options['limit'] * ($this->_options['page'] - 1); 316 | } 317 | } 318 | 319 | /** 320 | * return document with _id = $primaKey 321 | * 322 | * @param string $primaryKey 323 | * @return array|object 324 | * @access public 325 | */ 326 | public function get($primaryKey) 327 | { 328 | $this->_options['where']['_id'] = new ObjectId($primaryKey); 329 | 330 | return $this->findFirst(); 331 | } 332 | 333 | /** 334 | * return number of rows finded 335 | * 336 | * @return int 337 | * @access public 338 | */ 339 | public function count() 340 | { 341 | return $this->_totalRows; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/ORM/Table.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDriver(); 26 | if (!$driver instanceof \Hayko\Mongodb\Database\Driver\Mongodb) { 27 | throw new \Exception("Driver must be an instance of 'Hayko\Mongodb\Database\Driver\Mongodb'"); 28 | } 29 | $collection = $driver->getCollection($this->getTable()); 30 | 31 | return $collection; 32 | } 33 | 34 | /** 35 | * always return true because mongo is schemaless 36 | * 37 | * @param string $field 38 | * @return bool 39 | * @access public 40 | */ 41 | public function hasField($field) 42 | { 43 | return true; 44 | } 45 | 46 | public function aggregate(array $pipeline){ 47 | return $this->__getCollection()->aggregate($pipeline); 48 | } 49 | 50 | public function bulkWrite(array $operations){ 51 | return $this->__getCollection()->bulkWrite($operations); 52 | } 53 | 54 | public function updateMany($filter, array $operations){ 55 | try{ 56 | $update = $this->__getCollection()->updateMany($filter, $operations); 57 | return $update->getModifiedCount(); 58 | 59 | } catch (\Exception $e) { 60 | trigger_error($e->getMessage()); 61 | return false; 62 | } 63 | 64 | } 65 | 66 | public function updateOne($filter, array $operations){ 67 | try{ 68 | $update = $this->__getCollection()->updateOne($filter, $operations); 69 | return $update->getModifiedCount(); 70 | 71 | } catch (\Exception $e) { 72 | trigger_error($e->getMessage()); 73 | return false; 74 | } 75 | } 76 | 77 | /** 78 | * find documents 79 | * 80 | * @param string $type 81 | * @param array $options 82 | * @return \Cake\ORM\Entity|\Cake\ORM\Entity[]|MongoQuery 83 | * @access public 84 | * @throws \Exception 85 | */ 86 | public function find($type = 'all', $options = []) 87 | { 88 | $query = new MongoFinder($this->__getCollection(), $options); 89 | $alias = $this->getAlias(); 90 | if(gettype($type)=='array'){ 91 | $mongoCursor = $query->find($type, $options); 92 | if ($mongoCursor instanceof \MongoDB\Model\BSONDocument) { 93 | return (new Document($mongoCursor, $alias))->cakefy(); 94 | } elseif (is_null($mongoCursor) || is_array($mongoCursor)) { 95 | return $mongoCursor; 96 | } 97 | $results = new ResultSet($mongoCursor, $alias); 98 | if (isset($options['whitelist'])) { 99 | return new MongoQuery($results->toArray(), $query->count()); 100 | } else { 101 | return $results->toArray(); 102 | } 103 | 104 | }elseif (gettype($type)=='string'){ 105 | $method = 'find' . ucfirst($type); 106 | if(method_exists($query, $method)){ 107 | $mongoCursor = $query->{$method}(); 108 | if ($mongoCursor instanceof \MongoDB\Model\BSONDocument) { 109 | return (new Document($mongoCursor, $alias))->cakefy(); 110 | } elseif (is_null($mongoCursor) || is_array($mongoCursor)) { 111 | return $mongoCursor; 112 | } 113 | $results = new ResultSet($mongoCursor, $alias); 114 | 115 | if (isset($options['whitelist'])) { 116 | return new MongoQuery($results->toArray(), $query->count()); 117 | } else { 118 | return $results->toArray(); 119 | } 120 | } 121 | } 122 | 123 | throw new BadMethodCallException( 124 | sprintf('Unknown method "%s"', $method) 125 | ); 126 | } 127 | 128 | /** 129 | * get the document by _id 130 | * 131 | * @param string $primaryKey 132 | * @param array $options 133 | * @return \Cake\ORM\Entity 134 | * @access public 135 | * @throws \Exception 136 | */ 137 | public function get($primaryKey, $options = []) 138 | { 139 | $query = new MongoFinder($this->__getCollection(), $options); 140 | $result = $query->get($primaryKey); 141 | if ($result) { 142 | $document = new Document($result, $this->getAlias()); 143 | return $document->cakefy(); 144 | } 145 | 146 | throw new InvalidPrimaryKeyException(sprintf( 147 | 'Record not found in table "%s" with primary key [%s]', 148 | $this->_table, 149 | $primaryKey 150 | )); 151 | } 152 | 153 | /** 154 | * remove one document 155 | * 156 | * @param \Cake\Datasource\EntityInterface $entity 157 | * @param array $options 158 | * @return bool 159 | * @access public 160 | */ 161 | public function delete(EntityInterface $entity, $options = []) 162 | { 163 | try { 164 | $collection = $this->__getCollection(); 165 | $delete = $collection->deleteOne(['_id' => new \MongoDB\BSON\ObjectId($entity->_id)]); 166 | return (bool)$delete->getDeletedCount(); 167 | } catch (\Exception $e) { 168 | trigger_error($e->getMessage()); 169 | return false; 170 | } 171 | } 172 | 173 | /** 174 | * delete all rows matching $conditions 175 | * @param $conditions 176 | * @return int 177 | * @throws \Exception 178 | */ 179 | public function deleteAll($conditions) 180 | { 181 | try { 182 | $collection = $this->__getCollection(); 183 | $query = new MongoFinder($collection, ['where' => $conditions]); 184 | $rows = $query->find(['projection' => ['_id' => 1]]); 185 | $ids = []; 186 | foreach ($rows as $row) { 187 | $ids[] = $row->_id; 188 | } 189 | $delete = $collection->deleteMany(['_id' => ['$in' => $ids]]); 190 | return $delete->getDeletedCount(); 191 | } catch (\Exception $e) { 192 | trigger_error($e->getMessage()); 193 | return false; 194 | } 195 | } 196 | 197 | /** 198 | * save the document 199 | * 200 | * @param EntityInterface $entity 201 | * @param array $options 202 | * @return mixed $success 203 | * @access public 204 | * @throws \Exception 205 | */ 206 | public function save(EntityInterface $entity, $options = []) 207 | { 208 | 209 | $options = new ArrayObject($options + [ 210 | 'checkRules' => true, 211 | 'checkExisting' => true, 212 | '_primary' => true 213 | ]); 214 | 215 | if ($entity->getErrors()) { 216 | return false; 217 | } 218 | 219 | if ($entity->isNew() === false && !$entity->isDirty()) { 220 | return $entity; 221 | } 222 | $success = $this->_processSave($entity, $options); 223 | if ($success) { 224 | if ($options['_primary']) { 225 | $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options')); 226 | $entity->isNew(false); 227 | $entity->setSource($this->getRegistryAlias()); 228 | } 229 | } 230 | 231 | return $success; 232 | } 233 | 234 | /** 235 | * insert or update the document 236 | * 237 | * @param EntityInterface $entity 238 | * @param array|ArrayObject $options 239 | * @return mixed $success 240 | * @access protected 241 | * @throws \Exception 242 | */ 243 | protected function _processSave($entity, $options) 244 | { 245 | $mode = $entity->isNew() ? RulesChecker::CREATE : RulesChecker::UPDATE; 246 | if ($options['checkRules'] && !$this->checkRules($entity, $mode, $options)) { 247 | return false; 248 | } 249 | 250 | $event = $this->dispatchEvent('Model.beforeSave', compact('entity', 'options')); 251 | if ($event->isStopped()) { 252 | return $event->result; 253 | } 254 | 255 | $data = $entity->toArray(); 256 | $isNew = $entity->isNew(); 257 | 258 | //convert to mongodate 259 | /** @var ChronosInterface $c */ 260 | if (isset($data['created'])) { 261 | $c = $data['created']; 262 | $data['created'] = new \MongoDB\BSON\UTCDateTime(strtotime($c->toDateTimeString())); 263 | } 264 | if (isset($data['modified'])) { 265 | $c = $data['modified']; 266 | $data['modified'] = new \MongoDB\BSON\UTCDateTime(strtotime($c->toDateTimeString())); 267 | } 268 | if ($isNew) { 269 | $success = $this->_insert($entity, $data); 270 | } else { 271 | $success = $this->_update($entity, $data); 272 | } 273 | 274 | if ($success) { 275 | $this->dispatchEvent('Model.afterSave', compact('entity', 'options')); 276 | $entity->clean(); 277 | if (!$options['_primary']) { 278 | $entity->isNew(false); 279 | $entity->setSource($this->getRegistryAlias()); 280 | } 281 | 282 | $success = true; 283 | } 284 | 285 | if (!$success && $isNew) { 286 | $entity->unsetProperty($this->getPrimaryKey()); 287 | $entity->isNew(true); 288 | } 289 | 290 | if ($success) { 291 | return $entity; 292 | } 293 | 294 | return false; 295 | } 296 | 297 | /** 298 | * insert new document 299 | * 300 | * @param EntityInterface $entity 301 | * @param array $data 302 | * @return mixed $success 303 | * @access protected 304 | * @throws \Exception 305 | */ 306 | protected function _insert($entity, $data) 307 | { 308 | $primary = (array)$this->getPrimaryKey(); 309 | if (empty($primary)) { 310 | $msg = sprintf( 311 | 'Cannot insert row in "%s" table, it has no primary key.', 312 | $this->getTable() 313 | ); 314 | throw new RuntimeException($msg); 315 | } 316 | $primary = ['_id' => $this->_newId($primary)]; 317 | 318 | $filteredKeys = array_filter($primary, 'strlen'); 319 | $data = $data + $filteredKeys; 320 | 321 | $success = false; 322 | if (empty($data)) { 323 | return $success; 324 | } 325 | 326 | $success = $entity; 327 | $collection = $this->__getCollection(); 328 | 329 | if (is_object($collection)) { 330 | $result = $collection->insertOne($data); 331 | if ($result->isAcknowledged()) { 332 | $entity->set('_id', $result->getInsertedId()); 333 | } 334 | } 335 | return $success; 336 | } 337 | 338 | /** 339 | * update one document 340 | * 341 | * @param EntityInterface $entity 342 | * @param array $data 343 | * @return mixed $success 344 | * @access protected 345 | * @throws \Exception 346 | */ 347 | protected function _update($entity, $data) 348 | { unset($data['_id']); 349 | 350 | $update = $this->__getCollection()->updateOne( 351 | ['_id' => new \MongoDB\BSON\ObjectId($entity->_id)], 352 | ['$set' => $data] 353 | ); 354 | return (bool)$update->getModifiedCount(); 355 | } 356 | 357 | /** 358 | * Update $fields for rows matching $conditions 359 | * @param array $fields 360 | * @param array $conditions 361 | * @return bool|int|null 362 | */ 363 | public function updateAll($fields, $conditions) 364 | { 365 | try { 366 | $collection = $this->__getCollection(); 367 | $query = new MongoFinder($collection, ['where' => $conditions]); 368 | $rows = $query->find(['projection' => ['_id' => 1]]); 369 | $ids = []; 370 | foreach ($rows as $row) { 371 | $ids[] = $row->_id; 372 | } 373 | $data = [ 374 | '$set' => $fields 375 | ]; 376 | $update = $collection->updateMany(['_id' => ['$in' => $ids]], $data); 377 | return $update->getModifiedCount(); 378 | } catch (\Exception $e) { 379 | trigger_error($e->getMessage()); 380 | return false; 381 | } 382 | } 383 | 384 | /** 385 | * create new MongoDB\BSON\ObjectId 386 | * 387 | * @param mixed $primary 388 | * @return \MongoDB\BSON\ObjectId 389 | * @access public 390 | */ 391 | protected function _newId($primary) 392 | { 393 | if (!$primary || count((array)$primary) > 1) { 394 | return null; 395 | } 396 | 397 | return new \MongoDB\BSON\ObjectId(); 398 | } 399 | 400 | 401 | } 402 | 403 | --------------------------------------------------------------------------------