├── .gitignore ├── src └── sonvq │ └── Cassandra │ ├── Query │ ├── Grammar.php │ ├── Processor.php │ └── Builder.php │ ├── Eloquent │ ├── SoftDeletes.php │ ├── Builder.php │ ├── HybridRelations.php │ └── Model.php │ ├── Auth │ ├── PasswordBrokerManager.php │ ├── PasswordResetServiceProvider.php │ └── DatabaseTokenRepository.php │ ├── CassandraServiceProvider.php │ ├── Queue │ ├── CassandraConnector.php │ └── CassandraQueue.php │ ├── Relations │ ├── BelongsTo.php │ ├── MorphTo.php │ ├── HasOne.php │ ├── HasMany.php │ ├── EmbedsOne.php │ ├── BelongsToMany.php │ ├── EmbedsMany.php │ └── EmbedsOneOrMany.php │ ├── Collection.php │ ├── Schema │ ├── Builder.php │ └── Blueprint.php │ └── Connection.php ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/private/ 2 | /nbproject 3 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Query/Grammar.php: -------------------------------------------------------------------------------- 1 | getDeletedAtColumn(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Auth/PasswordBrokerManager.php: -------------------------------------------------------------------------------- 1 | app['db']->connection(), 19 | $config['table'], 20 | $this->app['config']['app.key'], 21 | $config['expire'] 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sonvq/cassandra", 3 | "description": "Cassandra Eloquent model and Query builder for Laravel", 4 | "keywords": ["laravel","eloquent","cassandra","database","model"], 5 | "homepage": "https://github.com/sonvq-qsoftvn/laravel-cassandra", 6 | "authors": [ 7 | { 8 | "name": "Quang Son Pro", 9 | "homepage": "https://byant.co.uk" 10 | } 11 | ], 12 | "license" : "MIT", 13 | "require": { 14 | "illuminate/support": "5.2.*", 15 | "illuminate/container": "5.2.*", 16 | "illuminate/database": "5.2.*", 17 | "illuminate/events": "5.2.*" 18 | }, 19 | "require-dev": { 20 | 21 | }, 22 | "autoload": { 23 | "psr-0": { 24 | "sonvq\\Cassandra": "src/", 25 | "sonvq\\Eloquent": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "classmap": [ 30 | "tests/TestCase.php", 31 | "tests/models", 32 | "tests/seeds" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/CassandraServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['db']); 17 | 18 | Model::setEventDispatcher($this->app['events']); 19 | } 20 | 21 | /** 22 | * Register the service provider. 23 | */ 24 | public function register() 25 | { 26 | // Add database driver. 27 | $this->app->resolving('db', function ($db) { 28 | $db->extend('cassandra', function ($config) { 29 | return new Connection($config); 30 | }); 31 | }); 32 | 33 | // Add connector for queue support. 34 | $this->app->resolving('queue', function ($queue) { 35 | $queue->addConnector('cassandra', function () { 36 | return new CassandraConnector($this->app['db']); 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Queue/CassandraConnector.php: -------------------------------------------------------------------------------- 1 | connections = $connections; 27 | } 28 | 29 | /** 30 | * Establish a queue connection. 31 | * 32 | * @param array $config 33 | * @return \Illuminate\Contracts\Queue\Queue 34 | */ 35 | public function connect(array $config) 36 | { 37 | return new CassandraQueue( 38 | $this->connections->connection(Arr::get($config, 'connection')), 39 | $config['table'], 40 | $config['queue'], 41 | Arr::get($config, 'expire', 60) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/BelongsTo.php: -------------------------------------------------------------------------------- 1 | query->where($this->otherKey, '=', $this->parent->{$this->foreignKey}); 17 | } 18 | } 19 | 20 | /** 21 | * Set the constraints for an eager load of the relation. 22 | * 23 | * @param array $models 24 | */ 25 | public function addEagerConstraints(array $models) 26 | { 27 | // We'll grab the primary key name of the related models since it could be set to 28 | // a non-standard name and not "id". We will then construct the constraint for 29 | // our eagerly loading query so it returns the proper models from execution. 30 | $key = $this->otherKey; 31 | 32 | $this->query->whereIn($key, $this->getEagerModelKeys($models)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/MorphTo.php: -------------------------------------------------------------------------------- 1 | query->where($this->otherKey, '=', $this->parent->{$this->foreignKey}); 19 | } 20 | } 21 | 22 | /** 23 | * Get all of the relation results for a type. 24 | * 25 | * @param string $type 26 | * @return \Illuminate\Database\Eloquent\Collection 27 | */ 28 | protected function getResultsByType($type) 29 | { 30 | $instance = $this->createModelByType($type); 31 | 32 | $key = $instance->getKeyName(); 33 | 34 | $query = $instance->newQuery(); 35 | 36 | $query = $this->useWithTrashed($query); 37 | 38 | return $query->whereIn($key, $this->gatherKeysByType($type)->all())->get(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/HasOne.php: -------------------------------------------------------------------------------- 1 | getHasCompareKey(); 20 | 21 | return $query->select($this->getHasCompareKey())->where($this->getHasCompareKey(), 'exists', true); 22 | } 23 | 24 | /** 25 | * Add the constraints for a relationship query. 26 | * 27 | * @param \Illuminate\Database\Eloquent\Builder $query 28 | * @param \Illuminate\Database\Eloquent\Builder $parent 29 | * @param array|mixed $columns 30 | * @return \Illuminate\Database\Eloquent\Builder 31 | */ 32 | public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) 33 | { 34 | $query->select($columns); 35 | 36 | $key = $this->wrap($this->getQualifiedParentKeyName()); 37 | 38 | return $query->where($this->getHasCompareKey(), 'exists', true); 39 | } 40 | 41 | /** 42 | * Get the plain foreign key. 43 | * 44 | * @return string 45 | */ 46 | public function getPlainForeignKey() 47 | { 48 | return $this->getForeignKey(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/HasMany.php: -------------------------------------------------------------------------------- 1 | getHasCompareKey(); 20 | 21 | return $query->select($this->getHasCompareKey())->where($this->getHasCompareKey(), 'exists', true); 22 | } 23 | 24 | /** 25 | * Add the constraints for a relationship query. 26 | * 27 | * @param \Illuminate\Database\Eloquent\Builder $query 28 | * @param \Illuminate\Database\Eloquent\Builder $parent 29 | * @param array|mixed $columns 30 | * @return \Illuminate\Database\Eloquent\Builder 31 | */ 32 | public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) 33 | { 34 | $query->select($columns); 35 | 36 | $key = $this->wrap($this->getQualifiedParentKeyName()); 37 | 38 | return $query->where($this->getHasCompareKey(), 'exists', true); 39 | } 40 | 41 | /** 42 | * Get the plain foreign key. 43 | * 44 | * @return string 45 | */ 46 | public function getPlainForeignKey() 47 | { 48 | return $this->getForeignKey(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Auth/PasswordResetServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('auth.password.tokens', function ($app) { 17 | $connection = $app['db']->connection(); 18 | 19 | // The database token repository is an implementation of the token repository 20 | // interface, and is responsible for the actual storing of auth tokens and 21 | // their e-mail addresses. We will inject this table and hash key to it. 22 | $table = $app['config']['auth.password.table']; 23 | 24 | $key = $app['config']['app.key']; 25 | 26 | $expire = $app['config']->get('auth.password.expire', 60); 27 | 28 | return new DatabaseTokenRepository($connection, $table, $key, $expire); 29 | }); 30 | } 31 | 32 | /** 33 | * Register the password broker instance. 34 | * 35 | * @return void 36 | */ 37 | protected function registerPasswordBroker() 38 | { 39 | $this->app->singleton('auth.password', function ($app) { 40 | return new PasswordBrokerManager($app); 41 | }); 42 | 43 | $this->app->bind('auth.password.broker', function ($app) { 44 | return $app->make('auth.password')->broker(); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Auth/DatabaseTokenRepository.php: -------------------------------------------------------------------------------- 1 | $email, 'token' => $token, 'created_at' => new UTCDateTime(round(microtime(true) * 1000))]; 20 | } 21 | 22 | /** 23 | * Determine if the token has expired. 24 | * 25 | * @param array $token 26 | * @return bool 27 | */ 28 | protected function tokenExpired($token) 29 | { 30 | // Convert UTCDateTime to a date string. 31 | if ($token['created_at'] instanceof UTCDateTime) { 32 | $date = $token['created_at']->toDateTime(); 33 | $date->setTimezone(new \DateTimeZone(date_default_timezone_get())); 34 | $token['created_at'] = $date->format('Y-m-d H:i:s'); 35 | } elseif (is_array($token['created_at']) and isset($token['created_at']['date'])) { 36 | $date = new DateTime($token['created_at']['date'], new DateTimeZone(isset($token['created_at']['timezone']) ? $token['created_at']['timezone'] : 'UTC')); 37 | $date->setTimezone(date_default_timezone_get()); 38 | $token['created_at'] = $date->format('Y-m-d H:i:s'); 39 | } 40 | 41 | return parent::tokenExpired($token); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Collection.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 30 | $this->collection = $collection; 31 | } 32 | 33 | /** 34 | * Handle dynamic method calls. 35 | * 36 | * @param string $method 37 | * @param array $parameters 38 | * @return mixed 39 | */ 40 | public function __call($method, $parameters) 41 | { 42 | $start = microtime(true); 43 | $result = call_user_func_array([$this->collection, $method], $parameters); 44 | 45 | if ($this->connection->logging()) { 46 | // Once we have run the query we will calculate the time that it took to run and 47 | // then log the query, bindings, and execution time so we will report them on 48 | // the event that the developer needs them. We'll log time in milliseconds. 49 | $time = $this->connection->getElapsedTime($start); 50 | 51 | $query = []; 52 | 53 | // Convert the query paramters to a json string. 54 | foreach ($parameters as $parameter) { 55 | try { 56 | $query[] = json_encode($parameter); 57 | } catch (Exception $e) { 58 | $query[] = '{...}'; 59 | } 60 | } 61 | 62 | $queryString = $this->collection->getCollectionName() . '.' . $method . '(' . implode(',', $query) . ')'; 63 | 64 | $this->connection->logQuery($queryString, [], $time); 65 | } 66 | 67 | return $result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Queue/CassandraQueue.php: -------------------------------------------------------------------------------- 1 | database->table($this->table) 19 | ->lockForUpdate() 20 | ->where('queue', $this->getQueue($queue)) 21 | ->where('reserved', 0) 22 | ->where('available_at', '<=', $this->getTime()) 23 | ->orderBy('id', 'asc') 24 | ->first(); 25 | 26 | if ($job) { 27 | $job = (object) $job; 28 | $job->id = $job->_id; 29 | } 30 | 31 | return $job ?: null; 32 | } 33 | 34 | /** 35 | * Release the jobs that have been reserved for too long. 36 | * 37 | * @param string $queue 38 | * @return void 39 | */ 40 | protected function releaseJobsThatHaveBeenReservedTooLong($queue) 41 | { 42 | $expired = Carbon::now()->subSeconds($this->expire)->getTimestamp(); 43 | 44 | $reserved = $this->database->collection($this->table) 45 | ->where('queue', $this->getQueue($queue)) 46 | ->where('reserved', 1) 47 | ->where('reserved_at', '<=', $expired)->get(); 48 | 49 | foreach ($reserved as $job) { 50 | $attempts = $job['attempts'] + 1; 51 | $this->releaseJob($job['_id'], $attempts); 52 | } 53 | } 54 | 55 | /** 56 | * Release the given job ID from reservation. 57 | * 58 | * @param string $id 59 | * 60 | * @return void 61 | */ 62 | protected function releaseJob($id, $attempts) 63 | { 64 | $this->database->table($this->table)->where('_id', $id)->update([ 65 | 'reserved' => 0, 66 | 'reserved_at' => null, 67 | 'attempts' => $attempts, 68 | ]); 69 | } 70 | 71 | /** 72 | * Mark the given job ID as reserved. 73 | * 74 | * @param string $id 75 | * @return void 76 | */ 77 | protected function markJobAsReserved($id) 78 | { 79 | $this->database->collection($this->table)->where('_id', $id)->update([ 80 | 'reserved' => 1, 'reserved_at' => $this->getTime(), 81 | ]); 82 | } 83 | 84 | /** 85 | * Delete a reserved job from the queue. 86 | * 87 | * @param string $queue 88 | * @param string $id 89 | * @return void 90 | */ 91 | public function deleteReserved($queue, $id) 92 | { 93 | $this->database->table($this->table)->where('_id', $id)->delete(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Schema/Builder.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 18 | } 19 | 20 | /** 21 | * Determine if the given table has a given column. 22 | * 23 | * @param string $table 24 | * @param string $column 25 | * @return bool 26 | */ 27 | public function hasColumn($table, $column) 28 | { 29 | return true; 30 | } 31 | 32 | /** 33 | * Determine if the given table has given columns. 34 | * 35 | * @param string $table 36 | * @param array $columns 37 | * @return bool 38 | */ 39 | public function hasColumns($table, array $columns) 40 | { 41 | return true; 42 | } 43 | 44 | /** 45 | * Determine if the given collection exists. 46 | * 47 | * @param string $collection 48 | * @return bool 49 | */ 50 | public function hasCollection($collection) 51 | { 52 | $db = $this->connection->getCassandra(); 53 | 54 | foreach ($db->listCollections() as $collectionFromCassandra) { 55 | if ($collectionFromCassandra->getName() == $collection) { 56 | return true; 57 | } 58 | } 59 | 60 | return false; 61 | } 62 | 63 | /** 64 | * Determine if the given collection exists. 65 | * 66 | * @param string $collection 67 | * @return bool 68 | */ 69 | public function hasTable($collection) 70 | { 71 | return $this->hasCollection($collection); 72 | } 73 | 74 | /** 75 | * Modify a collection on the schema. 76 | * 77 | * @param string $collection 78 | * @param Closure $callback 79 | * @return bool 80 | */ 81 | public function collection($collection, Closure $callback) 82 | { 83 | $blueprint = $this->createBlueprint($collection); 84 | 85 | if ($callback) { 86 | $callback($blueprint); 87 | } 88 | } 89 | 90 | /** 91 | * Modify a collection on the schema. 92 | * 93 | * @param string $collection 94 | * @param Closure $callback 95 | * @return bool 96 | */ 97 | public function table($collection, Closure $callback) 98 | { 99 | return $this->collection($collection, $callback); 100 | } 101 | 102 | /** 103 | * Create a new collection on the schema. 104 | * 105 | * @param string $collection 106 | * @param Closure $callback 107 | * @return bool 108 | */ 109 | public function create($collection, Closure $callback = null) 110 | { 111 | $blueprint = $this->createBlueprint($collection); 112 | 113 | $blueprint->create(); 114 | 115 | if ($callback) { 116 | $callback($blueprint); 117 | } 118 | } 119 | 120 | /** 121 | * Drop a collection from the schema. 122 | * 123 | * @param string $collection 124 | * @return bool 125 | */ 126 | public function drop($collection) 127 | { 128 | $blueprint = $this->createBlueprint($collection); 129 | 130 | return $blueprint->drop(); 131 | } 132 | 133 | /** 134 | * Create a new Blueprint. 135 | * 136 | * @param string $collection 137 | * @return Schema\Blueprint 138 | */ 139 | protected function createBlueprint($collection, Closure $callback = null) 140 | { 141 | return new Blueprint($this->connection, $collection); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/EmbedsOne.php: -------------------------------------------------------------------------------- 1 | setRelation($relation, null); 20 | } 21 | 22 | return $models; 23 | } 24 | 25 | /** 26 | * Get the results of the relationship. 27 | * 28 | * @return \Illuminate\Database\Eloquent\Model 29 | */ 30 | public function getResults() 31 | { 32 | return $this->toModel($this->getEmbedded()); 33 | } 34 | 35 | /** 36 | * Save a new model and attach it to the parent model. 37 | * 38 | * @param \Illuminate\Database\Eloquent\Model $model 39 | * @return \Illuminate\Database\Eloquent\Model 40 | */ 41 | public function performInsert(Model $model) 42 | { 43 | // Generate a new key if needed. 44 | if ($model->getKeyName() == '_id' and ! $model->getKey()) { 45 | $model->setAttribute('_id', new ObjectID); 46 | } 47 | 48 | // For deeply nested documents, let the parent handle the changes. 49 | if ($this->isNested()) { 50 | $this->associate($model); 51 | 52 | return $this->parent->save(); 53 | } 54 | 55 | $result = $this->getBaseQuery()->update([$this->localKey => $model->getAttributes()]); 56 | 57 | // Attach the model to its parent. 58 | if ($result) { 59 | $this->associate($model); 60 | } 61 | 62 | return $result ? $model : false; 63 | } 64 | 65 | /** 66 | * Save an existing model and attach it to the parent model. 67 | * 68 | * @param \Illuminate\Database\Eloquent\Model $model 69 | * @return \Illuminate\Database\Eloquent\Model|bool 70 | */ 71 | public function performUpdate(Model $model) 72 | { 73 | if ($this->isNested()) { 74 | $this->associate($model); 75 | 76 | return $this->parent->save(); 77 | } 78 | 79 | // Use array dot notation for better update behavior. 80 | $values = array_dot($model->getDirty(), $this->localKey . '.'); 81 | 82 | $result = $this->getBaseQuery()->update($values); 83 | 84 | // Attach the model to its parent. 85 | if ($result) { 86 | $this->associate($model); 87 | } 88 | 89 | return $result ? $model : false; 90 | } 91 | 92 | /** 93 | * Delete an existing model and detach it from the parent model. 94 | * 95 | * @param \Illuminate\Database\Eloquent\Model $model 96 | * @return int 97 | */ 98 | public function performDelete(Model $model) 99 | { 100 | // For deeply nested documents, let the parent handle the changes. 101 | if ($this->isNested()) { 102 | $this->dissociate($model); 103 | 104 | return $this->parent->save(); 105 | } 106 | 107 | // Overwrite the local key with an empty array. 108 | $result = $this->getBaseQuery()->update([$this->localKey => null]); 109 | 110 | // Detach the model from its parent. 111 | if ($result) { 112 | $this->dissociate(); 113 | } 114 | 115 | return $result; 116 | } 117 | 118 | /** 119 | * Attach the model to its parent. 120 | * 121 | * @param \Illuminate\Database\Eloquent\Model $model 122 | * @return \Illuminate\Database\Eloquent\Model 123 | */ 124 | public function associate(Model $model) 125 | { 126 | return $this->setEmbedded($model->getAttributes()); 127 | } 128 | 129 | /** 130 | * Detach the model from its parent. 131 | * 132 | * @return \Illuminate\Database\Eloquent\Model 133 | */ 134 | public function dissociate() 135 | { 136 | return $this->setEmbedded(null); 137 | } 138 | 139 | /** 140 | * Delete all embedded models. 141 | * 142 | * @return int 143 | */ 144 | public function delete() 145 | { 146 | $model = $this->getResults(); 147 | 148 | return $this->performDelete($model); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Schema/Blueprint.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 40 | 41 | $this->collection = $connection->getCollection($collection); 42 | } 43 | 44 | /** 45 | * Specify an index for the collection. 46 | * 47 | * @param string|array $columns 48 | * @param array $options 49 | * @return Blueprint 50 | */ 51 | public function index($columns = null, $options = []) 52 | { 53 | $columns = $this->fluent($columns); 54 | 55 | // Columns are passed as a default array. 56 | if (is_array($columns) && is_int(key($columns))) { 57 | // Transform the columns to the required array format. 58 | $transform = []; 59 | 60 | foreach ($columns as $column) { 61 | $transform[$column] = 1; 62 | } 63 | 64 | $columns = $transform; 65 | } 66 | 67 | $this->collection->createIndex($columns, $options); 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Specify the primary key(s) for the table. 74 | * 75 | * @param string|array $columns 76 | * @param array $options 77 | * @return \Illuminate\Support\Fluent 78 | */ 79 | public function primary($columns = null, $options = []) 80 | { 81 | return $this->unique($columns, $options); 82 | } 83 | 84 | /** 85 | * Indicate that the given index should be dropped. 86 | * 87 | * @param string|array $columns 88 | * @return Blueprint 89 | */ 90 | public function dropIndex($columns = null) 91 | { 92 | $columns = $this->fluent($columns); 93 | 94 | // Columns are passed as a default array. 95 | if (is_array($columns) && is_int(key($columns))) { 96 | // Transform the columns to the required array format. 97 | $transform = []; 98 | 99 | foreach ($columns as $column) { 100 | $transform[$column] = $column . '_1'; 101 | } 102 | 103 | $columns = $transform; 104 | } 105 | 106 | foreach ($columns as $column) { 107 | $this->collection->dropIndex($column); 108 | } 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Specify a unique index for the collection. 115 | * 116 | * @param string|array $columns 117 | * @param array $options 118 | * @return Blueprint 119 | */ 120 | public function unique($columns = null, $options = []) 121 | { 122 | $columns = $this->fluent($columns); 123 | 124 | $options['unique'] = true; 125 | 126 | $this->index($columns, $options); 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Specify a non blocking index for the collection. 133 | * 134 | * @param string|array $columns 135 | * @return Blueprint 136 | */ 137 | public function background($columns = null) 138 | { 139 | $columns = $this->fluent($columns); 140 | 141 | $this->index($columns, ['background' => true]); 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * Specify a sparse index for the collection. 148 | * 149 | * @param string|array $columns 150 | * @param array $options 151 | * @return Blueprint 152 | */ 153 | public function sparse($columns = null, $options = []) 154 | { 155 | $columns = $this->fluent($columns); 156 | 157 | $options['sparse'] = true; 158 | 159 | $this->index($columns, $options); 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Specify the number of seconds after wich a document should be considered expired based, 166 | * on the given single-field index containing a date. 167 | * 168 | * @param string|array $columns 169 | * @param int $seconds 170 | * @return Blueprint 171 | */ 172 | public function expire($columns, $seconds) 173 | { 174 | $columns = $this->fluent($columns); 175 | 176 | $this->index($columns, ['expireAfterSeconds' => $seconds]); 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Indicate that the table needs to be created. 183 | * 184 | * @return bool 185 | */ 186 | public function create() 187 | { 188 | $collection = $this->collection->getCollectionName(); 189 | 190 | $db = $this->connection->getCassandra(); 191 | 192 | // Ensure the collection is created. 193 | $db->createCollection($collection); 194 | } 195 | 196 | /** 197 | * Indicate that the collection should be dropped. 198 | * 199 | * @return bool 200 | */ 201 | public function drop() 202 | { 203 | $this->collection->drop(); 204 | } 205 | 206 | /** 207 | * Add a new column to the blueprint. 208 | * 209 | * @param string $type 210 | * @param string $name 211 | * @param array $parameters 212 | * @return Blueprint 213 | */ 214 | public function addColumn($type, $name, array $parameters = []) 215 | { 216 | $this->fluent($name); 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * Allow fluent columns. 223 | * 224 | * @param string|array $columns 225 | * @return string|array 226 | */ 227 | protected function fluent($columns = null) 228 | { 229 | if (is_null($columns)) { 230 | return $this->columns; 231 | } elseif (is_string($columns)) { 232 | return $this->columns = [$columns]; 233 | } else { 234 | return $this->columns = $columns; 235 | } 236 | } 237 | 238 | /** 239 | * Allows the use of unsupported schema methods. 240 | * 241 | * @return Blueprint 242 | */ 243 | public function __call($method, $args) 244 | { 245 | // Dummy. 246 | return $this; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Connection.php: -------------------------------------------------------------------------------- 1 | config = $config; 31 | 32 | // You can pass options directly to the Cassandra constructor 33 | $options = array_get($config, 'options', []); 34 | 35 | // Create the connection 36 | $this->connection = $this->createConnection(null, $config, $options); 37 | 38 | // Select database 39 | $this->db = $this->connection->selectDatabase($config['database']); 40 | 41 | $this->useDefaultPostProcessor(); 42 | } 43 | 44 | /** 45 | * Get the default post processor instance. 46 | * 47 | * @return Query\Processor 48 | */ 49 | protected function getDefaultPostProcessor() 50 | { 51 | return new Query\Processor; 52 | } 53 | 54 | /** 55 | * Begin a fluent query against a database collection. 56 | * 57 | * @param string $collection 58 | * @return Query\Builder 59 | */ 60 | public function collection($collection) 61 | { 62 | $processor = $this->getPostProcessor(); 63 | 64 | $query = new Query\Builder($this, $processor); 65 | 66 | return $query->from($collection); 67 | } 68 | 69 | /** 70 | * Begin a fluent query against a database collection. 71 | * 72 | * @param string $table 73 | * @return Query\Builder 74 | */ 75 | public function table($table) 76 | { 77 | return $this->collection($table); 78 | } 79 | 80 | /** 81 | * Get a Cassandra collection. 82 | * 83 | * @param string $name 84 | * @return Collection 85 | */ 86 | public function getCollection($name) 87 | { 88 | return new Collection($this, $this->db->selectCollection($name)); 89 | } 90 | 91 | /** 92 | * Get a schema builder instance for the connection. 93 | * 94 | * @return Schema\Builder 95 | */ 96 | public function getSchemaBuilder() 97 | { 98 | return new Schema\Builder($this); 99 | } 100 | 101 | /** 102 | * Get the Cassandra database object. 103 | * 104 | * @return \Cassandra\Database 105 | */ 106 | public function getCassandra() 107 | { 108 | return $this->db; 109 | } 110 | 111 | /** 112 | * return Cassandra object. 113 | * 114 | * @return \Cassandra\Client 115 | */ 116 | public function getCassandraClient() 117 | { 118 | return $this->connection; 119 | } 120 | 121 | /** 122 | * Create a new Cassandra connection. 123 | * 124 | * @param string $dsn 125 | * @param array $config 126 | * @param array $options 127 | * @return Cassandra 128 | */ 129 | protected function createConnection($dsn, array $config, array $options) 130 | { 131 | // By default driver options is an empty array. 132 | $driverOptions = []; 133 | 134 | if (isset($config['driver_options']) && is_array($config['driver_options'])) { 135 | $driverOptions = $config['driver_options']; 136 | } 137 | 138 | // Check if the credentials are not already set in the options 139 | if (!isset($options['username']) && !empty($config['username'])) { 140 | $options['username'] = $config['username']; 141 | } 142 | if (!isset($options['password']) && !empty($config['password'])) { 143 | $options['password'] = $config['password']; 144 | } 145 | 146 | 147 | /*return new Client($dsn, $options, $driverOptions);*/ 148 | 149 | $cluster = Cassandra::cluster(); 150 | 151 | // Authentication 152 | if (isset($options['username']) && isset($options['password'])) { 153 | $cluster->withCredentials($options['username'], $options['password']); 154 | 155 | } 156 | 157 | // Contact Points 158 | if (isset($options['contactpoints']) || ( isset($config['contactpoints']) && !empty($config['contactpoints']))) { 159 | $contactPoints = $config['contactpoints']; 160 | if (isset($options['contactpoints'])) { 161 | $contactPoints = $options['contactpoints']; 162 | } 163 | $cluster->withContactPoints(explode(',', $contactPoints)); 164 | } 165 | 166 | 167 | 168 | $cluster->build(); 169 | $session = $cluster->connect(); 170 | 171 | return $session; 172 | } 173 | 174 | /** 175 | * Disconnect from the underlying Cassandra connection. 176 | */ 177 | public function disconnect() 178 | { 179 | unset($this->connection); 180 | } 181 | 182 | /** 183 | * Create a DSN string from a configuration. 184 | * 185 | * @param array $config 186 | * @return string 187 | */ 188 | protected function getDsn(array $config) 189 | { 190 | // First we will create the basic DSN setup as well as the port if it is in 191 | // in the configuration options. This will give us the basic DSN we will 192 | // need to establish the Cassandra and return them back for use. 193 | extract($config); 194 | 195 | // Check if the user passed a complete dsn to the configuration. 196 | if (! empty($dsn)) { 197 | return $dsn; 198 | } 199 | 200 | // Treat host option as array of hosts 201 | $hosts = is_array($host) ? $host : [$host]; 202 | 203 | foreach ($hosts as &$host) { 204 | // Check if we need to add a port to the host 205 | if (strpos($host, ':') === false and isset($port)) { 206 | $host = "{$host}:{$port}"; 207 | } 208 | } 209 | 210 | return "cassandra://" . implode(',', $hosts) . "/{$database}"; 211 | } 212 | 213 | /** 214 | * Get the elapsed time since a given starting point. 215 | * 216 | * @param int $start 217 | * @return float 218 | */ 219 | public function getElapsedTime($start) 220 | { 221 | return parent::getElapsedTime($start); 222 | } 223 | 224 | /** 225 | * Get the PDO driver name. 226 | * 227 | * @return string 228 | */ 229 | public function getDriverName() 230 | { 231 | return 'cassandra'; 232 | } 233 | 234 | /** 235 | * Dynamically pass methods to the connection. 236 | * 237 | * @param string $method 238 | * @param array $parameters 239 | * @return mixed 240 | */ 241 | public function __call($method, $parameters) 242 | { 243 | return call_user_func_array([$this->db, $method], $parameters); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Eloquent/Builder.php: -------------------------------------------------------------------------------- 1 | model->getParentRelation()) { 34 | $relation->performUpdate($this->model, $values); 35 | 36 | return 1; 37 | } 38 | 39 | return $this->query->update($this->addUpdatedAtColumn($values), $options); 40 | } 41 | 42 | /** 43 | * Insert a new record into the database. 44 | * 45 | * @param array $values 46 | * @return bool 47 | */ 48 | public function insert(array $values) 49 | { 50 | // Intercept operations on embedded models and delegate logic 51 | // to the parent relation instance. 52 | if ($relation = $this->model->getParentRelation()) { 53 | $relation->performInsert($this->model, $values); 54 | 55 | return true; 56 | } 57 | 58 | return parent::insert($values); 59 | } 60 | 61 | /** 62 | * Insert a new record and get the value of the primary key. 63 | * 64 | * @param array $values 65 | * @param string $sequence 66 | * @return int 67 | */ 68 | public function insertGetId(array $values, $sequence = null) 69 | { 70 | // Intercept operations on embedded models and delegate logic 71 | // to the parent relation instance. 72 | if ($relation = $this->model->getParentRelation()) { 73 | $relation->performInsert($this->model, $values); 74 | 75 | return $this->model->getKey(); 76 | } 77 | 78 | return parent::insertGetId($values, $sequence); 79 | } 80 | 81 | /** 82 | * Delete a record from the database. 83 | * 84 | * @return mixed 85 | */ 86 | public function delete() 87 | { 88 | // Intercept operations on embedded models and delegate logic 89 | // to the parent relation instance. 90 | if ($relation = $this->model->getParentRelation()) { 91 | $relation->performDelete($this->model); 92 | 93 | return $this->model->getKey(); 94 | } 95 | 96 | return parent::delete(); 97 | } 98 | 99 | /** 100 | * Increment a column's value by a given amount. 101 | * 102 | * @param string $column 103 | * @param int $amount 104 | * @param array $extra 105 | * @return int 106 | */ 107 | public function increment($column, $amount = 1, array $extra = []) 108 | { 109 | // Intercept operations on embedded models and delegate logic 110 | // to the parent relation instance. 111 | if ($relation = $this->model->getParentRelation()) { 112 | $value = $this->model->{$column}; 113 | 114 | // When doing increment and decrements, Eloquent will automatically 115 | // sync the original attributes. We need to change the attribute 116 | // temporary in order to trigger an update query. 117 | $this->model->{$column} = null; 118 | 119 | $this->model->syncOriginalAttribute($column); 120 | 121 | $result = $this->model->update([$column => $value]); 122 | 123 | return $result; 124 | } 125 | 126 | return parent::increment($column, $amount, $extra); 127 | } 128 | 129 | /** 130 | * Decrement a column's value by a given amount. 131 | * 132 | * @param string $column 133 | * @param int $amount 134 | * @param array $extra 135 | * @return int 136 | */ 137 | public function decrement($column, $amount = 1, array $extra = []) 138 | { 139 | // Intercept operations on embedded models and delegate logic 140 | // to the parent relation instance. 141 | if ($relation = $this->model->getParentRelation()) { 142 | $value = $this->model->{$column}; 143 | 144 | // When doing increment and decrements, Eloquent will automatically 145 | // sync the original attributes. We need to change the attribute 146 | // temporary in order to trigger an update query. 147 | $this->model->{$column} = null; 148 | 149 | $this->model->syncOriginalAttribute($column); 150 | 151 | return $this->model->update([$column => $value]); 152 | } 153 | 154 | return parent::decrement($column, $amount, $extra); 155 | } 156 | 157 | /** 158 | * Add the "has" condition where clause to the query. 159 | * 160 | * @param \Illuminate\Database\Eloquent\Builder $hasQuery 161 | * @param \Illuminate\Database\Eloquent\Relations\Relation $relation 162 | * @param string $operator 163 | * @param int $count 164 | * @param string $boolean 165 | * @return \Illuminate\Database\Eloquent\Builder 166 | */ 167 | protected function addHasWhere(EloquentBuilder $hasQuery, Relation $relation, $operator, $count, $boolean) 168 | { 169 | $query = $hasQuery->getQuery(); 170 | 171 | // Get the number of related objects for each possible parent. 172 | $relationCount = array_count_values($query->lists($relation->getHasCompareKey())); 173 | 174 | // Remove unwanted related objects based on the operator and count. 175 | $relationCount = array_filter($relationCount, function ($counted) use ($count, $operator) { 176 | // If we are comparing to 0, we always need all results. 177 | if ($count == 0) { 178 | return true; 179 | } 180 | 181 | switch ($operator) { 182 | case '>=': 183 | case '<': 184 | return $counted >= $count; 185 | case '>': 186 | case '<=': 187 | return $counted > $count; 188 | case '=': 189 | case '!=': 190 | return $counted == $count; 191 | } 192 | }); 193 | 194 | // If the operator is <, <= or !=, we will use whereNotIn. 195 | $not = in_array($operator, ['<', '<=', '!=']); 196 | 197 | // If we are comparing to 0, we need an additional $not flip. 198 | if ($count == 0) { 199 | $not = !$not; 200 | } 201 | 202 | // All related ids. 203 | $relatedIds = array_keys($relationCount); 204 | 205 | // Add whereIn to the query. 206 | return $this->whereIn($this->model->getKeyName(), $relatedIds, $boolean, $not); 207 | } 208 | 209 | /** 210 | * Create a raw database expression. 211 | * 212 | * @param closure $expression 213 | * @return mixed 214 | */ 215 | public function raw($expression = null) 216 | { 217 | // Get raw results from the query builder. 218 | $results = $this->query->raw($expression); 219 | 220 | // Convert CassandraCursor results to a collection of models. 221 | if ($results instanceof Cursor) { 222 | $results = iterator_to_array($results, false); 223 | return $this->model->hydrate($results); 224 | } // Convert Cassandra BSONDocument to a single object. 225 | elseif ($results instanceof BSONDocument) { 226 | $results = $results->getArrayCopy(); 227 | return $this->model->newFromBuilder((array) $results); 228 | } // The result is a single object. 229 | elseif (is_array($results) and array_key_exists('_id', $results)) { 230 | return $this->model->newFromBuilder((array) $results); 231 | } 232 | 233 | return $results; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/BelongsToMany.php: -------------------------------------------------------------------------------- 1 | setWhere(); 39 | } 40 | } 41 | 42 | /** 43 | * Set the where clause for the relation query. 44 | * 45 | * @return $this 46 | */ 47 | protected function setWhere() 48 | { 49 | $foreign = $this->getForeignKey(); 50 | 51 | $this->query->where($foreign, '=', $this->parent->getKey()); 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Save a new model and attach it to the parent model. 58 | * 59 | * @param \Illuminate\Database\Eloquent\Model $model 60 | * @param array $joining 61 | * @param bool $touch 62 | * @return \Illuminate\Database\Eloquent\Model 63 | */ 64 | public function save(Model $model, array $joining = [], $touch = true) 65 | { 66 | $model->save(['touch' => false]); 67 | 68 | $this->attach($model, $joining, $touch); 69 | 70 | return $model; 71 | } 72 | 73 | /** 74 | * Create a new instance of the related model. 75 | * 76 | * @param array $attributes 77 | * @param array $joining 78 | * @param bool $touch 79 | * @return \Illuminate\Database\Eloquent\Model 80 | */ 81 | public function create(array $attributes, array $joining = [], $touch = true) 82 | { 83 | $instance = $this->related->newInstance($attributes); 84 | 85 | // Once we save the related model, we need to attach it to the base model via 86 | // through intermediate table so we'll use the existing "attach" method to 87 | // accomplish this which will insert the record and any more attributes. 88 | $instance->save(['touch' => false]); 89 | 90 | $this->attach($instance, $joining, $touch); 91 | 92 | return $instance; 93 | } 94 | 95 | /** 96 | * Sync the intermediate tables with a list of IDs or collection of models. 97 | * 98 | * @param array $ids 99 | * @param bool $detaching 100 | * @return array 101 | */ 102 | public function sync($ids, $detaching = true) 103 | { 104 | $changes = [ 105 | 'attached' => [], 'detached' => [], 'updated' => [], 106 | ]; 107 | 108 | if ($ids instanceof Collection) { 109 | $ids = $ids->modelKeys(); 110 | } 111 | 112 | // First we need to attach any of the associated models that are not currently 113 | // in this joining table. We'll spin through the given IDs, checking to see 114 | // if they exist in the array of current ones, and if not we will insert. 115 | $current = $this->parent->{$this->otherKey} ?: []; 116 | 117 | // See issue #256. 118 | if ($current instanceof Collection) { 119 | $current = $ids->modelKeys(); 120 | } 121 | 122 | $records = $this->formatSyncList($ids); 123 | 124 | $detach = array_diff($current, array_keys($records)); 125 | 126 | // We need to make sure we pass a clean array, so that it is not interpreted 127 | // as an associative array. 128 | $detach = array_values($detach); 129 | 130 | // Next, we will take the differences of the currents and given IDs and detach 131 | // all of the entities that exist in the "current" array but are not in the 132 | // the array of the IDs given to the method which will complete the sync. 133 | if ($detaching and count($detach) > 0) { 134 | $this->detach($detach); 135 | 136 | $changes['detached'] = (array) array_map(function ($v) { 137 | return is_numeric($v) ? (int) $v : (string) $v; 138 | }, $detach); 139 | } 140 | 141 | // Now we are finally ready to attach the new records. Note that we'll disable 142 | // touching until after the entire operation is complete so we don't fire a 143 | // ton of touch operations until we are totally done syncing the records. 144 | $changes = array_merge( 145 | $changes, 146 | $this->attachNew($records, $current, false) 147 | ); 148 | 149 | if (count($changes['attached']) || count($changes['updated'])) { 150 | $this->touchIfTouching(); 151 | } 152 | 153 | return $changes; 154 | } 155 | 156 | /** 157 | * Update an existing pivot record on the table. 158 | * 159 | * @param mixed $id 160 | * @param array $attributes 161 | * @param bool $touch 162 | */ 163 | public function updateExistingPivot($id, array $attributes, $touch = true) 164 | { 165 | // Do nothing, we have no pivot table. 166 | } 167 | 168 | /** 169 | * Attach a model to the parent. 170 | * 171 | * @param mixed $id 172 | * @param array $attributes 173 | * @param bool $touch 174 | */ 175 | public function attach($id, array $attributes = [], $touch = true) 176 | { 177 | if ($id instanceof Model) { 178 | $model = $id; 179 | 180 | $id = $model->getKey(); 181 | 182 | // Attach the new parent id to the related model. 183 | $model->push($this->foreignKey, $this->parent->getKey(), true); 184 | } else { 185 | $query = $this->newRelatedQuery(); 186 | 187 | $query->whereIn($this->related->getKeyName(), (array) $id); 188 | 189 | // Attach the new parent id to the related model. 190 | $query->push($this->foreignKey, $this->parent->getKey(), true); 191 | } 192 | 193 | // Attach the new ids to the parent model. 194 | $this->parent->push($this->otherKey, (array) $id, true); 195 | 196 | if ($touch) { 197 | $this->touchIfTouching(); 198 | } 199 | } 200 | 201 | /** 202 | * Detach models from the relationship. 203 | * 204 | * @param int|array $ids 205 | * @param bool $touch 206 | * @return int 207 | */ 208 | public function detach($ids = [], $touch = true) 209 | { 210 | if ($ids instanceof Model) { 211 | $ids = (array) $ids->getKey(); 212 | } 213 | 214 | $query = $this->newRelatedQuery(); 215 | 216 | // If associated IDs were passed to the method we will only delete those 217 | // associations, otherwise all of the association ties will be broken. 218 | // We'll return the numbers of affected rows when we do the deletes. 219 | $ids = (array) $ids; 220 | 221 | // Detach all ids from the parent model. 222 | $this->parent->pull($this->otherKey, $ids); 223 | 224 | // Prepare the query to select all related objects. 225 | if (count($ids) > 0) { 226 | $query->whereIn($this->related->getKeyName(), $ids); 227 | } 228 | 229 | // Remove the relation to the parent. 230 | $query->pull($this->foreignKey, $this->parent->getKey()); 231 | 232 | if ($touch) { 233 | $this->touchIfTouching(); 234 | } 235 | 236 | return count($ids); 237 | } 238 | 239 | /** 240 | * Build model dictionary keyed by the relation's foreign key. 241 | * 242 | * @param \Illuminate\Database\Eloquent\Collection $results 243 | * @return array 244 | */ 245 | protected function buildDictionary(Collection $results) 246 | { 247 | $foreign = $this->foreignKey; 248 | 249 | // First we will build a dictionary of child models keyed by the foreign key 250 | // of the relation so that we will easily and quickly match them to their 251 | // parents without having a possibly slow inner loops for every models. 252 | $dictionary = []; 253 | 254 | foreach ($results as $result) { 255 | foreach ($result->$foreign as $item) { 256 | $dictionary[$item][] = $result; 257 | } 258 | } 259 | 260 | return $dictionary; 261 | } 262 | 263 | /** 264 | * Create a new query builder for the related model. 265 | * 266 | * @return \Illuminate\Database\Query\Builder 267 | */ 268 | protected function newPivotQuery() 269 | { 270 | return $this->newRelatedQuery(); 271 | } 272 | 273 | /** 274 | * Create a new query builder for the related model. 275 | * 276 | * @return \Illuminate\Database\Query\Builder 277 | */ 278 | public function newRelatedQuery() 279 | { 280 | return $this->related->newQuery(); 281 | } 282 | 283 | /** 284 | * Get the fully qualified foreign key for the relation. 285 | * 286 | * @return string 287 | */ 288 | public function getForeignKey() 289 | { 290 | return $this->foreignKey; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/EmbedsMany.php: -------------------------------------------------------------------------------- 1 | setRelation($relation, $this->related->newCollection()); 22 | } 23 | 24 | return $models; 25 | } 26 | 27 | /** 28 | * Get the results of the relationship. 29 | * 30 | * @return \Illuminate\Database\Eloquent\Collection 31 | */ 32 | public function getResults() 33 | { 34 | return $this->toCollection($this->getEmbedded()); 35 | } 36 | 37 | /** 38 | * Save a new model and attach it to the parent model. 39 | * 40 | * @param \Illuminate\Database\Eloquent\Model $model 41 | * @return \Illuminate\Database\Eloquent\Model 42 | */ 43 | public function performInsert(Model $model) 44 | { 45 | // Generate a new key if needed. 46 | if ($model->getKeyName() == '_id' and ! $model->getKey()) { 47 | $model->setAttribute('_id', new ObjectID); 48 | } 49 | 50 | // For deeply nested documents, let the parent handle the changes. 51 | if ($this->isNested()) { 52 | $this->associate($model); 53 | 54 | return $this->parent->save(); 55 | } 56 | 57 | // Push the new model to the database. 58 | $result = $this->getBaseQuery()->push($this->localKey, $model->getAttributes(), true); 59 | 60 | // Attach the model to its parent. 61 | if ($result) { 62 | $this->associate($model); 63 | } 64 | 65 | return $result ? $model : false; 66 | } 67 | 68 | /** 69 | * Save an existing model and attach it to the parent model. 70 | * 71 | * @param \Illuminate\Database\Eloquent\Model $model 72 | * @return Model|bool 73 | */ 74 | public function performUpdate(Model $model) 75 | { 76 | // For deeply nested documents, let the parent handle the changes. 77 | if ($this->isNested()) { 78 | $this->associate($model); 79 | 80 | return $this->parent->save(); 81 | } 82 | 83 | // Get the correct foreign key value. 84 | $foreignKey = $this->getForeignKeyValue($model); 85 | 86 | // Use array dot notation for better update behavior. 87 | $values = array_dot($model->getDirty(), $this->localKey . '.$.'); 88 | 89 | // Update document in database. 90 | $result = $this->getBaseQuery()->where($this->localKey . '.' . $model->getKeyName(), $foreignKey) 91 | ->update($values); 92 | 93 | // Attach the model to its parent. 94 | if ($result) { 95 | $this->associate($model); 96 | } 97 | 98 | return $result ? $model : false; 99 | } 100 | 101 | /** 102 | * Delete an existing model and detach it from the parent model. 103 | * 104 | * @param Model $model 105 | * @return int 106 | */ 107 | public function performDelete(Model $model) 108 | { 109 | // For deeply nested documents, let the parent handle the changes. 110 | if ($this->isNested()) { 111 | $this->dissociate($model); 112 | 113 | return $this->parent->save(); 114 | } 115 | 116 | // Get the correct foreign key value. 117 | $foreignKey = $this->getForeignKeyValue($model); 118 | 119 | $result = $this->getBaseQuery()->pull($this->localKey, [$model->getKeyName() => $foreignKey]); 120 | 121 | if ($result) { 122 | $this->dissociate($model); 123 | } 124 | 125 | return $result; 126 | } 127 | 128 | /** 129 | * Associate the model instance to the given parent, without saving it to the database. 130 | * 131 | * @param \Illuminate\Database\Eloquent\Model $model 132 | * @return \Illuminate\Database\Eloquent\Model 133 | */ 134 | public function associate(Model $model) 135 | { 136 | if (! $this->contains($model)) { 137 | return $this->associateNew($model); 138 | } else { 139 | return $this->associateExisting($model); 140 | } 141 | } 142 | 143 | /** 144 | * Dissociate the model instance from the given parent, without saving it to the database. 145 | * 146 | * @param mixed $ids 147 | * @return int 148 | */ 149 | public function dissociate($ids = []) 150 | { 151 | $ids = $this->getIdsArrayFrom($ids); 152 | 153 | $records = $this->getEmbedded(); 154 | 155 | $primaryKey = $this->related->getKeyName(); 156 | 157 | // Remove the document from the parent model. 158 | foreach ($records as $i => $record) { 159 | if (in_array($record[$primaryKey], $ids)) { 160 | unset($records[$i]); 161 | } 162 | } 163 | 164 | $this->setEmbedded($records); 165 | 166 | // We return the total number of deletes for the operation. The developers 167 | // can then check this number as a boolean type value or get this total count 168 | // of records deleted for logging, etc. 169 | return count($ids); 170 | } 171 | 172 | /** 173 | * Destroy the embedded models for the given IDs. 174 | * 175 | * @param mixed $ids 176 | * @return int 177 | */ 178 | public function destroy($ids = []) 179 | { 180 | $count = 0; 181 | 182 | $ids = $this->getIdsArrayFrom($ids); 183 | 184 | // Get all models matching the given ids. 185 | $models = $this->getResults()->only($ids); 186 | 187 | // Pull the documents from the database. 188 | foreach ($models as $model) { 189 | if ($model->delete()) { 190 | $count++; 191 | } 192 | } 193 | 194 | return $count; 195 | } 196 | 197 | /** 198 | * Delete all embedded models. 199 | * 200 | * @return int 201 | */ 202 | public function delete() 203 | { 204 | // Overwrite the local key with an empty array. 205 | $result = $this->query->update([$this->localKey => []]); 206 | 207 | if ($result) { 208 | $this->setEmbedded([]); 209 | } 210 | 211 | return $result; 212 | } 213 | 214 | /** 215 | * Destroy alias. 216 | * 217 | * @param mixed $ids 218 | * @return int 219 | */ 220 | public function detach($ids = []) 221 | { 222 | return $this->destroy($ids); 223 | } 224 | 225 | /** 226 | * Save alias. 227 | * 228 | * @param \Illuminate\Database\Eloquent\Model $model 229 | * @return \Illuminate\Database\Eloquent\Model 230 | */ 231 | public function attach(Model $model) 232 | { 233 | return $this->save($model); 234 | } 235 | 236 | /** 237 | * Associate a new model instance to the given parent, without saving it to the database. 238 | * 239 | * @param \Illuminate\Database\Eloquent\Model $model 240 | * @return \Illuminate\Database\Eloquent\Model 241 | */ 242 | protected function associateNew($model) 243 | { 244 | // Create a new key if needed. 245 | if (! $model->getAttribute('_id')) { 246 | $model->setAttribute('_id', new ObjectID); 247 | } 248 | 249 | $records = $this->getEmbedded(); 250 | 251 | // Add the new model to the embedded documents. 252 | $records[] = $model->getAttributes(); 253 | 254 | return $this->setEmbedded($records); 255 | } 256 | 257 | /** 258 | * Associate an existing model instance to the given parent, without saving it to the database. 259 | * 260 | * @param \Illuminate\Database\Eloquent\Model $model 261 | * @return \Illuminate\Database\Eloquent\Model 262 | */ 263 | protected function associateExisting($model) 264 | { 265 | // Get existing embedded documents. 266 | $records = $this->getEmbedded(); 267 | 268 | $primaryKey = $this->related->getKeyName(); 269 | 270 | $key = $model->getKey(); 271 | 272 | // Replace the document in the parent model. 273 | foreach ($records as &$record) { 274 | if ($record[$primaryKey] == $key) { 275 | $record = $model->getAttributes(); 276 | break; 277 | } 278 | } 279 | 280 | return $this->setEmbedded($records); 281 | } 282 | 283 | /** 284 | * Get a paginator for the "select" statement. 285 | * 286 | * @param int $perPage 287 | * @return \Illuminate\Pagination\Paginator 288 | */ 289 | public function paginate($perPage = null) 290 | { 291 | $page = Paginator::resolveCurrentPage(); 292 | $perPage = $perPage ?: $this->related->getPerPage(); 293 | 294 | $results = $this->getEmbedded(); 295 | 296 | $total = count($results); 297 | 298 | $start = ($page - 1) * $perPage; 299 | $sliced = array_slice($results, $start, $perPage); 300 | 301 | return new LengthAwarePaginator($sliced, $total, $perPage, $page, [ 302 | 'path' => Paginator::resolveCurrentPath(), 303 | ]); 304 | } 305 | 306 | /** 307 | * Get the embedded records array. 308 | * 309 | * @return array 310 | */ 311 | protected function getEmbedded() 312 | { 313 | return parent::getEmbedded() ?: []; 314 | } 315 | 316 | /** 317 | * Set the embedded records array. 318 | * 319 | * @param array $models 320 | */ 321 | protected function setEmbedded($models) 322 | { 323 | if (! is_array($models)) { 324 | $models = [$models]; 325 | } 326 | 327 | return parent::setEmbedded(array_values($models)); 328 | } 329 | 330 | /** 331 | * Handle dynamic method calls to the relationship. 332 | * 333 | * @param string $method 334 | * @param array $parameters 335 | * @return mixed 336 | */ 337 | public function __call($method, $parameters) 338 | { 339 | if (method_exists('Illuminate\Database\Eloquent\Collection', $method)) { 340 | return call_user_func_array([$this->getResults(), $method], $parameters); 341 | } 342 | 343 | return parent::__call($method, $parameters); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Eloquent/HybridRelations.php: -------------------------------------------------------------------------------- 1 | getForeignKey(); 32 | 33 | $instance = new $related; 34 | 35 | $localKey = $localKey ?: $this->getKeyName(); 36 | 37 | return new HasOne($instance->newQuery(), $this, $foreignKey, $localKey); 38 | } 39 | 40 | /** 41 | * Define a polymorphic one-to-one relationship. 42 | * 43 | * @param string $related 44 | * @param string $name 45 | * @param string $type 46 | * @param string $id 47 | * @param string $localKey 48 | * @return \Illuminate\Database\Eloquent\Relations\MorphOne 49 | */ 50 | public function morphOne($related, $name, $type = null, $id = null, $localKey = null) 51 | { 52 | // Check if it is a relation with an original model. 53 | if (! is_subclass_of($related, 'sonvq\Cassandra\Eloquent\Model')) { 54 | return parent::morphOne($related, $name, $type, $id, $localKey); 55 | } 56 | 57 | $instance = new $related; 58 | 59 | list($type, $id) = $this->getMorphs($name, $type, $id); 60 | 61 | $table = $instance->getTable(); 62 | 63 | $localKey = $localKey ?: $this->getKeyName(); 64 | 65 | return new MorphOne($instance->newQuery(), $this, $type, $id, $localKey); 66 | } 67 | 68 | /** 69 | * Define a one-to-many relationship. 70 | * 71 | * @param string $related 72 | * @param string $foreignKey 73 | * @param string $localKey 74 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 75 | */ 76 | public function hasMany($related, $foreignKey = null, $localKey = null) 77 | { 78 | // Check if it is a relation with an original model. 79 | if (! is_subclass_of($related, 'sonvq\Cassandra\Eloquent\Model')) { 80 | return parent::hasMany($related, $foreignKey, $localKey); 81 | } 82 | 83 | $foreignKey = $foreignKey ?: $this->getForeignKey(); 84 | 85 | $instance = new $related; 86 | 87 | $localKey = $localKey ?: $this->getKeyName(); 88 | 89 | return new HasMany($instance->newQuery(), $this, $foreignKey, $localKey); 90 | } 91 | 92 | /** 93 | * Define a polymorphic one-to-many relationship. 94 | * 95 | * @param string $related 96 | * @param string $name 97 | * @param string $type 98 | * @param string $id 99 | * @param string $localKey 100 | * @return \Illuminate\Database\Eloquent\Relations\MorphMany 101 | */ 102 | public function morphMany($related, $name, $type = null, $id = null, $localKey = null) 103 | { 104 | // Check if it is a relation with an original model. 105 | if (! is_subclass_of($related, 'sonvq\Cassandra\Eloquent\Model')) { 106 | return parent::morphMany($related, $name, $type, $id, $localKey); 107 | } 108 | 109 | $instance = new $related; 110 | 111 | // Here we will gather up the morph type and ID for the relationship so that we 112 | // can properly query the intermediate table of a relation. Finally, we will 113 | // get the table and create the relationship instances for the developers. 114 | list($type, $id) = $this->getMorphs($name, $type, $id); 115 | 116 | $table = $instance->getTable(); 117 | 118 | $localKey = $localKey ?: $this->getKeyName(); 119 | 120 | return new MorphMany($instance->newQuery(), $this, $type, $id, $localKey); 121 | } 122 | 123 | /** 124 | * Define an inverse one-to-one or many relationship. 125 | * 126 | * @param string $related 127 | * @param string $foreignKey 128 | * @param string $otherKey 129 | * @param string $relation 130 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 131 | */ 132 | public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null) 133 | { 134 | // If no relation name was given, we will use this debug backtrace to extract 135 | // the calling method's name and use that as the relationship name as most 136 | // of the time this will be what we desire to use for the relationships. 137 | if (is_null($relation)) { 138 | list($current, $caller) = debug_backtrace(false, 2); 139 | 140 | $relation = $caller['function']; 141 | } 142 | 143 | // Check if it is a relation with an original model. 144 | if (! is_subclass_of($related, 'sonvq\Cassandra\Eloquent\Model')) { 145 | return parent::belongsTo($related, $foreignKey, $otherKey, $relation); 146 | } 147 | 148 | // If no foreign key was supplied, we can use a backtrace to guess the proper 149 | // foreign key name by using the name of the relationship function, which 150 | // when combined with an "_id" should conventionally match the columns. 151 | if (is_null($foreignKey)) { 152 | $foreignKey = Str::snake($relation) . '_id'; 153 | } 154 | 155 | $instance = new $related; 156 | 157 | // Once we have the foreign key names, we'll just create a new Eloquent query 158 | // for the related models and returns the relationship instance which will 159 | // actually be responsible for retrieving and hydrating every relations. 160 | $query = $instance->newQuery(); 161 | 162 | $otherKey = $otherKey ?: $instance->getKeyName(); 163 | 164 | return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); 165 | } 166 | 167 | /** 168 | * Define a polymorphic, inverse one-to-one or many relationship. 169 | * 170 | * @param string $name 171 | * @param string $type 172 | * @param string $id 173 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo 174 | */ 175 | public function morphTo($name = null, $type = null, $id = null) 176 | { 177 | // If no name is provided, we will use the backtrace to get the function name 178 | // since that is most likely the name of the polymorphic interface. We can 179 | // use that to get both the class and foreign key that will be utilized. 180 | if (is_null($name)) { 181 | list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 182 | 183 | $name = Str::snake($caller['function']); 184 | } 185 | 186 | list($type, $id) = $this->getMorphs($name, $type, $id); 187 | 188 | // If the type value is null it is probably safe to assume we're eager loading 189 | // the relationship. When that is the case we will pass in a dummy query as 190 | // there are multiple types in the morph and we can't use single queries. 191 | if (is_null($class = $this->$type)) { 192 | return new MorphTo( 193 | $this->newQuery(), 194 | $this, 195 | $id, 196 | null, 197 | $type, 198 | $name 199 | ); 200 | } // If we are not eager loading the relationship we will essentially treat this 201 | // as a belongs-to style relationship since morph-to extends that class and 202 | // we will pass in the appropriate values so that it behaves as expected. 203 | else { 204 | $class = $this->getActualClassNameForMorph($class); 205 | 206 | $instance = new $class; 207 | 208 | return new MorphTo( 209 | $instance->newQuery(), 210 | $this, 211 | $id, 212 | $instance->getKeyName(), 213 | $type, 214 | $name 215 | ); 216 | } 217 | } 218 | 219 | /** 220 | * Define a many-to-many relationship. 221 | * 222 | * @param string $related 223 | * @param string $collection 224 | * @param string $foreignKey 225 | * @param string $otherKey 226 | * @param string $relation 227 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 228 | */ 229 | public function belongsToMany($related, $collection = null, $foreignKey = null, $otherKey = null, $relation = null) 230 | { 231 | // If no relationship name was passed, we will pull backtraces to get the 232 | // name of the calling function. We will use that function name as the 233 | // title of this relation since that is a great convention to apply. 234 | if (is_null($relation)) { 235 | $relation = $this->getBelongsToManyCaller(); 236 | } 237 | 238 | // Check if it is a relation with an original model. 239 | if (! is_subclass_of($related, 'sonvq\Cassandra\Eloquent\Model')) { 240 | return parent::belongsToMany($related, $collection, $foreignKey, $otherKey, $relation); 241 | } 242 | 243 | // First, we'll need to determine the foreign key and "other key" for the 244 | // relationship. Once we have determined the keys we'll make the query 245 | // instances as well as the relationship instances we need for this. 246 | $foreignKey = $foreignKey ?: $this->getForeignKey() . 's'; 247 | 248 | $instance = new $related; 249 | 250 | $otherKey = $otherKey ?: $instance->getForeignKey() . 's'; 251 | 252 | // If no table name was provided, we can guess it by concatenating the two 253 | // models using underscores in alphabetical order. The two model names 254 | // are transformed to snake case from their default CamelCase also. 255 | if (is_null($collection)) { 256 | $collection = $instance->getTable(); 257 | } 258 | 259 | // Now we're ready to create a new query builder for the related model and 260 | // the relationship instances for the relation. The relations will set 261 | // appropriate query constraint and entirely manages the hydrations. 262 | $query = $instance->newQuery(); 263 | 264 | return new BelongsToMany($query, $this, $collection, $foreignKey, $otherKey, $relation); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Relations/EmbedsOneOrMany.php: -------------------------------------------------------------------------------- 1 | query = $query; 46 | $this->parent = $parent; 47 | $this->related = $related; 48 | $this->localKey = $localKey; 49 | $this->foreignKey = $foreignKey; 50 | $this->relation = $relation; 51 | 52 | // If this is a nested relation, we need to get the parent query instead. 53 | if ($parentRelation = $this->getParentRelation()) { 54 | $this->query = $parentRelation->getQuery(); 55 | } 56 | 57 | $this->addConstraints(); 58 | } 59 | 60 | /** 61 | * Set the base constraints on the relation query. 62 | */ 63 | public function addConstraints() 64 | { 65 | if (static::$constraints) { 66 | $this->query->where($this->getQualifiedParentKeyName(), '=', $this->getParentKey()); 67 | } 68 | } 69 | 70 | /** 71 | * Set the constraints for an eager load of the relation. 72 | * 73 | * @param array $models 74 | */ 75 | public function addEagerConstraints(array $models) 76 | { 77 | // There are no eager loading constraints. 78 | } 79 | 80 | /** 81 | * Match the eagerly loaded results to their parents. 82 | * 83 | * @param array $models 84 | * @param \Illuminate\Database\Eloquent\Collection $results 85 | * @param string $relation 86 | * @return array 87 | */ 88 | public function match(array $models, Collection $results, $relation) 89 | { 90 | foreach ($models as $model) { 91 | $results = $model->$relation()->getResults(); 92 | 93 | $model->setParentRelation($this); 94 | 95 | $model->setRelation($relation, $results); 96 | } 97 | 98 | return $models; 99 | } 100 | 101 | /** 102 | * Shorthand to get the results of the relationship. 103 | * 104 | * @return \Illuminate\Database\Eloquent\Collection 105 | */ 106 | public function get() 107 | { 108 | return $this->getResults(); 109 | } 110 | 111 | /** 112 | * Get the number of embedded models. 113 | * 114 | * @return int 115 | */ 116 | public function count() 117 | { 118 | return count($this->getEmbedded()); 119 | } 120 | 121 | /** 122 | * Attach a model instance to the parent model. 123 | * 124 | * @param \Illuminate\Database\Eloquent\Model $model 125 | * @return \Illuminate\Database\Eloquent\Model 126 | */ 127 | public function save(Model $model) 128 | { 129 | $model->setParentRelation($this); 130 | 131 | return $model->save() ? $model : false; 132 | } 133 | 134 | /** 135 | * Attach a collection of models to the parent instance. 136 | * 137 | * @param \Illuminate\Database\Eloquent\Collection|array $models 138 | * @return \Illuminate\Database\Eloquent\Collection|array 139 | */ 140 | public function saveMany($models) 141 | { 142 | foreach ($models as $model) { 143 | $this->save($model); 144 | } 145 | 146 | return $models; 147 | } 148 | 149 | /** 150 | * Create a new instance of the related model. 151 | * 152 | * @param array $attributes 153 | * @return \Illuminate\Database\Eloquent\Model 154 | */ 155 | public function create(array $attributes) 156 | { 157 | // Here we will set the raw attributes to avoid hitting the "fill" method so 158 | // that we do not have to worry about a mass accessor rules blocking sets 159 | // on the models. Otherwise, some of these attributes will not get set. 160 | $instance = $this->related->newInstance($attributes); 161 | 162 | $instance->setParentRelation($this); 163 | 164 | $instance->save(); 165 | 166 | return $instance; 167 | } 168 | 169 | /** 170 | * Create an array of new instances of the related model. 171 | * 172 | * @param array $records 173 | * @return array 174 | */ 175 | public function createMany(array $records) 176 | { 177 | $instances = []; 178 | 179 | foreach ($records as $record) { 180 | $instances[] = $this->create($record); 181 | } 182 | 183 | return $instances; 184 | } 185 | 186 | /** 187 | * Transform single ID, single Model or array of Models into an array of IDs. 188 | * 189 | * @param mixed $ids 190 | * @return array 191 | */ 192 | protected function getIdsArrayFrom($ids) 193 | { 194 | if ($ids instanceof \Illuminate\Support\Collection) { 195 | $ids = $ids->all(); 196 | } 197 | 198 | if (! is_array($ids)) { 199 | $ids = [$ids]; 200 | } 201 | 202 | foreach ($ids as &$id) { 203 | if ($id instanceof Model) { 204 | $id = $id->getKey(); 205 | } 206 | } 207 | 208 | return $ids; 209 | } 210 | 211 | /** 212 | * Get the embedded records array. 213 | * 214 | * @return array 215 | */ 216 | protected function getEmbedded() 217 | { 218 | // Get raw attributes to skip relations and accessors. 219 | $attributes = $this->parent->getAttributes(); 220 | 221 | // Get embedded models form parent attributes. 222 | $embedded = isset($attributes[$this->localKey]) ? (array) $attributes[$this->localKey] : null; 223 | 224 | return $embedded; 225 | } 226 | 227 | /** 228 | * Set the embedded records array. 229 | * 230 | * @param array $records 231 | * @return \Illuminate\Database\Eloquent\Model 232 | */ 233 | protected function setEmbedded($records) 234 | { 235 | // Assign models to parent attributes array. 236 | $attributes = $this->parent->getAttributes(); 237 | $attributes[$this->localKey] = $records; 238 | 239 | // Set raw attributes to skip mutators. 240 | $this->parent->setRawAttributes($attributes); 241 | 242 | // Set the relation on the parent. 243 | return $this->parent->setRelation($this->relation, $records === null ? null : $this->getResults()); 244 | } 245 | 246 | /** 247 | * Get the foreign key value for the relation. 248 | * 249 | * @param mixed $id 250 | * @return mixed 251 | */ 252 | protected function getForeignKeyValue($id) 253 | { 254 | if ($id instanceof Model) { 255 | $id = $id->getKey(); 256 | } 257 | 258 | // Convert the id to CassandraId if necessary. 259 | return $this->getBaseQuery()->convertKey($id); 260 | } 261 | 262 | /** 263 | * Convert an array of records to a Collection. 264 | * 265 | * @param array $records 266 | * @return \sonvq\Cassandra\Eloquent\Collection 267 | */ 268 | protected function toCollection(array $records = []) 269 | { 270 | $models = []; 271 | 272 | foreach ($records as $attributes) { 273 | $models[] = $this->toModel($attributes); 274 | } 275 | 276 | if (count($models) > 0) { 277 | $models = $this->eagerLoadRelations($models); 278 | } 279 | 280 | return new Collection($models); 281 | } 282 | 283 | /** 284 | * Create a related model instanced. 285 | * 286 | * @param array $attributes 287 | * @return \Illuminate\Database\Eloquent\Model 288 | */ 289 | protected function toModel($attributes = []) 290 | { 291 | if (is_null($attributes)) { 292 | return; 293 | } 294 | 295 | $model = $this->related->newFromBuilder((array) $attributes); 296 | 297 | $model->setParentRelation($this); 298 | 299 | $model->setRelation($this->foreignKey, $this->parent); 300 | 301 | // If you remove this, you will get segmentation faults! 302 | $model->setHidden(array_merge($model->getHidden(), [$this->foreignKey])); 303 | 304 | return $model; 305 | } 306 | 307 | /** 308 | * Get the relation instance of the parent. 309 | * 310 | * @return \Illuminate\Database\Eloquent\Relations\Relation 311 | */ 312 | protected function getParentRelation() 313 | { 314 | return $this->parent->getParentRelation(); 315 | } 316 | 317 | /** 318 | * Get the underlying query for the relation. 319 | * 320 | * @return \Illuminate\Database\Eloquent\Builder 321 | */ 322 | public function getQuery() 323 | { 324 | // Because we are sharing this relation instance to models, we need 325 | // to make sure we use separate query instances. 326 | return clone $this->query; 327 | } 328 | 329 | /** 330 | * Get the base query builder driving the Eloquent builder. 331 | * 332 | * @return \Illuminate\Database\Query\Builder 333 | */ 334 | public function getBaseQuery() 335 | { 336 | // Because we are sharing this relation instance to models, we need 337 | // to make sure we use separate query instances. 338 | return clone $this->query->getQuery(); 339 | } 340 | 341 | /** 342 | * Check if this relation is nested in another relation. 343 | * 344 | * @return bool 345 | */ 346 | protected function isNested() 347 | { 348 | return $this->getParentRelation() != null; 349 | } 350 | 351 | /** 352 | * Get the fully qualified local key name. 353 | * 354 | * @param string $glue 355 | * @return string 356 | */ 357 | protected function getPathHierarchy($glue = '.') 358 | { 359 | if ($parentRelation = $this->getParentRelation()) { 360 | return $parentRelation->getPathHierarchy($glue) . $glue . $this->localKey; 361 | } 362 | 363 | return $this->localKey; 364 | } 365 | 366 | /** 367 | * Get the parent's fully qualified key name. 368 | * 369 | * @return string 370 | */ 371 | public function getQualifiedParentKeyName() 372 | { 373 | if ($parentRelation = $this->getParentRelation()) { 374 | return $parentRelation->getPathHierarchy() . '.' . $this->parent->getKeyName(); 375 | } 376 | 377 | return $this->parent->getKeyName(); 378 | } 379 | 380 | /** 381 | * Get the primary key value of the parent. 382 | * 383 | * @return string 384 | */ 385 | protected function getParentKey() 386 | { 387 | return $this->parent->getKey(); 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Eloquent/Model.php: -------------------------------------------------------------------------------- 1 | attributes)) { 54 | $value = $this->attributes['_id']; 55 | } 56 | 57 | // Convert ObjectID to string. 58 | if ($value instanceof ObjectID) { 59 | return (string) $value; 60 | } 61 | 62 | return $value; 63 | } 64 | 65 | /** 66 | * Get the table qualified key name. 67 | * 68 | * @return string 69 | */ 70 | public function getQualifiedKeyName() 71 | { 72 | return $this->getKeyName(); 73 | } 74 | 75 | /** 76 | * Define an embedded one-to-many relationship. 77 | * 78 | * @param string $related 79 | * @param string $localKey 80 | * @param string $foreignKey 81 | * @param string $relation 82 | * @return \sonvq\Cassandra\Relations\EmbedsMany 83 | */ 84 | protected function embedsMany($related, $localKey = null, $foreignKey = null, $relation = null) 85 | { 86 | // If no relation name was given, we will use this debug backtrace to extract 87 | // the calling method's name and use that as the relationship name as most 88 | // of the time this will be what we desire to use for the relatinoships. 89 | if (is_null($relation)) { 90 | list(, $caller) = debug_backtrace(false); 91 | 92 | $relation = $caller['function']; 93 | } 94 | 95 | if (is_null($localKey)) { 96 | $localKey = $relation; 97 | } 98 | 99 | if (is_null($foreignKey)) { 100 | $foreignKey = snake_case(class_basename($this)); 101 | } 102 | 103 | $query = $this->newQuery(); 104 | 105 | $instance = new $related; 106 | 107 | return new EmbedsMany($query, $this, $instance, $localKey, $foreignKey, $relation); 108 | } 109 | 110 | /** 111 | * Define an embedded one-to-many relationship. 112 | * 113 | * @param string $related 114 | * @param string $localKey 115 | * @param string $foreignKey 116 | * @param string $relation 117 | * @return \sonvq\Cassandra\Relations\EmbedsOne 118 | */ 119 | protected function embedsOne($related, $localKey = null, $foreignKey = null, $relation = null) 120 | { 121 | // If no relation name was given, we will use this debug backtrace to extract 122 | // the calling method's name and use that as the relationship name as most 123 | // of the time this will be what we desire to use for the relatinoships. 124 | if (is_null($relation)) { 125 | list(, $caller) = debug_backtrace(false); 126 | 127 | $relation = $caller['function']; 128 | } 129 | 130 | if (is_null($localKey)) { 131 | $localKey = $relation; 132 | } 133 | 134 | if (is_null($foreignKey)) { 135 | $foreignKey = snake_case(class_basename($this)); 136 | } 137 | 138 | $query = $this->newQuery(); 139 | 140 | $instance = new $related; 141 | 142 | return new EmbedsOne($query, $this, $instance, $localKey, $foreignKey, $relation); 143 | } 144 | 145 | /** 146 | * Convert a DateTime to a storable UTCDateTime object. 147 | * 148 | * @param DateTime|int $value 149 | * @return UTCDateTime 150 | */ 151 | public function fromDateTime($value) 152 | { 153 | // If the value is already a UTCDateTime instance, we don't need to parse it. 154 | if ($value instanceof UTCDateTime) { 155 | return $value; 156 | } 157 | 158 | // Let Eloquent convert the value to a DateTime instance. 159 | if (! $value instanceof DateTime) { 160 | $value = parent::asDateTime($value); 161 | } 162 | 163 | return new UTCDateTime($value->getTimestamp() * 1000); 164 | } 165 | 166 | /** 167 | * Return a timestamp as DateTime object. 168 | * 169 | * @param mixed $value 170 | * @return DateTime 171 | */ 172 | protected function asDateTime($value) 173 | { 174 | // Convert UTCDateTime instances. 175 | if ($value instanceof UTCDateTime) { 176 | return Carbon::createFromTimestamp($value->toDateTime()->getTimestamp()); 177 | } 178 | 179 | return parent::asDateTime($value); 180 | } 181 | 182 | /** 183 | * Get the format for database stored dates. 184 | * 185 | * @return string 186 | */ 187 | protected function getDateFormat() 188 | { 189 | return $this->dateFormat ?: 'Y-m-d H:i:s'; 190 | } 191 | 192 | /** 193 | * Get a fresh timestamp for the model. 194 | * 195 | * @return UTCDateTime 196 | */ 197 | public function freshTimestamp() 198 | { 199 | return new UTCDateTime(round(microtime(true) * 1000)); 200 | } 201 | 202 | /** 203 | * Get the table associated with the model. 204 | * 205 | * @return string 206 | */ 207 | public function getTable() 208 | { 209 | return $this->collection ?: parent::getTable(); 210 | } 211 | 212 | /** 213 | * Get an attribute from the model. 214 | * 215 | * @param string $key 216 | * @return mixed 217 | */ 218 | public function getAttribute($key) 219 | { 220 | // Check if the key is an array dot notation. 221 | if (str_contains($key, '.') and array_has($this->attributes, $key)) { 222 | return $this->getAttributeValue($key); 223 | } 224 | 225 | $camelKey = camel_case($key); 226 | 227 | // If the "attribute" exists as a method on the model, it may be an 228 | // embedded model. If so, we need to return the result before it 229 | // is handled by the parent method. 230 | if (method_exists($this, $camelKey)) { 231 | $method = new ReflectionMethod(get_called_class(), $camelKey); 232 | 233 | // Ensure the method is not static to avoid conflicting with Eloquent methods. 234 | if (! $method->isStatic()) { 235 | $relations = $this->$camelKey(); 236 | 237 | // This attribute matches an embedsOne or embedsMany relation so we need 238 | // to return the relation results instead of the interal attributes. 239 | if ($relations instanceof EmbedsOneOrMany) { 240 | // If the key already exists in the relationships array, it just means the 241 | // relationship has already been loaded, so we'll just return it out of 242 | // here because there is no need to query within the relations twice. 243 | if (array_key_exists($key, $this->relations)) { 244 | return $this->relations[$key]; 245 | } 246 | 247 | // Get the relation results. 248 | return $this->getRelationshipFromMethod($key, $camelKey); 249 | } 250 | } 251 | } 252 | 253 | return parent::getAttribute($key); 254 | } 255 | 256 | /** 257 | * Get an attribute from the $attributes array. 258 | * 259 | * @param string $key 260 | * @return mixed 261 | */ 262 | protected function getAttributeFromArray($key) 263 | { 264 | // Support keys in dot notation. 265 | if (str_contains($key, '.')) { 266 | $attributes = array_dot($this->attributes); 267 | 268 | if (array_key_exists($key, $attributes)) { 269 | return $attributes[$key]; 270 | } 271 | } 272 | 273 | return parent::getAttributeFromArray($key); 274 | } 275 | 276 | /** 277 | * Set a given attribute on the model. 278 | * 279 | * @param string $key 280 | * @param mixed $value 281 | */ 282 | public function setAttribute($key, $value) 283 | { 284 | // Convert _id to ObjectID. 285 | if ($key == '_id' and is_string($value)) { 286 | $builder = $this->newBaseQueryBuilder(); 287 | 288 | $value = $builder->convertKey($value); 289 | } // Support keys in dot notation. 290 | elseif (str_contains($key, '.')) { 291 | if (in_array($key, $this->getDates()) && $value) { 292 | $value = $this->fromDateTime($value); 293 | } 294 | 295 | array_set($this->attributes, $key, $value); 296 | 297 | return; 298 | } 299 | 300 | parent::setAttribute($key, $value); 301 | } 302 | 303 | /** 304 | * Convert the model's attributes to an array. 305 | * 306 | * @return array 307 | */ 308 | public function attributesToArray() 309 | { 310 | $attributes = parent::attributesToArray(); 311 | 312 | // Because the original Eloquent never returns objects, we convert 313 | // Cassandra related objects to a string representation. This kind 314 | // of mimics the SQL behaviour so that dates are formatted 315 | // nicely when your models are converted to JSON. 316 | foreach ($attributes as $key => &$value) { 317 | if ($value instanceof ObjectID) { 318 | $value = (string) $value; 319 | } 320 | } 321 | 322 | // Convert dot-notation dates. 323 | foreach ($this->getDates() as $key) { 324 | if (str_contains($key, '.') and array_has($attributes, $key)) { 325 | array_set($attributes, $key, (string) $this->asDateTime(array_get($attributes, $key))); 326 | } 327 | } 328 | 329 | return $attributes; 330 | } 331 | 332 | /** 333 | * Get the casts array. 334 | * 335 | * @return array 336 | */ 337 | public function getCasts() 338 | { 339 | return $this->casts; 340 | } 341 | 342 | /** 343 | * Determine if the new and old values for a given key are numerically equivalent. 344 | * 345 | * @param string $key 346 | * @return bool 347 | */ 348 | protected function originalIsNumericallyEquivalent($key) 349 | { 350 | $current = $this->attributes[$key]; 351 | $original = $this->original[$key]; 352 | 353 | // Date comparison. 354 | if (in_array($key, $this->getDates())) { 355 | $current = $current instanceof UTCDateTime ? $this->asDateTime($current) : $current; 356 | $original = $original instanceof UTCDateTime ? $this->asDateTime($original) : $original; 357 | 358 | return $current == $original; 359 | } 360 | 361 | return parent::originalIsNumericallyEquivalent($key); 362 | } 363 | 364 | /** 365 | * Remove one or more fields. 366 | * 367 | * @param mixed $columns 368 | * @return int 369 | */ 370 | public function drop($columns) 371 | { 372 | if (! is_array($columns)) { 373 | $columns = [$columns]; 374 | } 375 | 376 | // Unset attributes 377 | foreach ($columns as $column) { 378 | $this->__unset($column); 379 | } 380 | 381 | // Perform unset only on current document 382 | return $this->newQuery()->where($this->getKeyName(), $this->getKey())->unset($columns); 383 | } 384 | 385 | /** 386 | * Append one or more values to an array. 387 | * 388 | * @return mixed 389 | */ 390 | public function push() 391 | { 392 | if ($parameters = func_get_args()) { 393 | $unique = false; 394 | 395 | if (count($parameters) == 3) { 396 | list($column, $values, $unique) = $parameters; 397 | } else { 398 | list($column, $values) = $parameters; 399 | } 400 | 401 | // Do batch push by default. 402 | if (! is_array($values)) { 403 | $values = [$values]; 404 | } 405 | 406 | $query = $this->setKeysForSaveQuery($this->newQuery()); 407 | 408 | $this->pushAttributeValues($column, $values, $unique); 409 | 410 | return $query->push($column, $values, $unique); 411 | } 412 | 413 | return parent::push(); 414 | } 415 | 416 | /** 417 | * Remove one or more values from an array. 418 | * 419 | * @param string $column 420 | * @param mixed $values 421 | * @return mixed 422 | */ 423 | public function pull($column, $values) 424 | { 425 | // Do batch pull by default. 426 | if (! is_array($values)) { 427 | $values = [$values]; 428 | } 429 | 430 | $query = $this->setKeysForSaveQuery($this->newQuery()); 431 | 432 | $this->pullAttributeValues($column, $values); 433 | 434 | return $query->pull($column, $values); 435 | } 436 | 437 | /** 438 | * Append one or more values to the underlying attribute value and sync with original. 439 | * 440 | * @param string $column 441 | * @param array $values 442 | * @param bool $unique 443 | */ 444 | protected function pushAttributeValues($column, array $values, $unique = false) 445 | { 446 | $current = $this->getAttributeFromArray($column) ?: []; 447 | 448 | foreach ($values as $value) { 449 | // Don't add duplicate values when we only want unique values. 450 | if ($unique and in_array($value, $current)) { 451 | continue; 452 | } 453 | 454 | array_push($current, $value); 455 | } 456 | 457 | $this->attributes[$column] = $current; 458 | 459 | $this->syncOriginalAttribute($column); 460 | } 461 | 462 | /** 463 | * Remove one or more values to the underlying attribute value and sync with original. 464 | * 465 | * @param string $column 466 | * @param array $values 467 | */ 468 | protected function pullAttributeValues($column, array $values) 469 | { 470 | $current = $this->getAttributeFromArray($column) ?: []; 471 | 472 | foreach ($values as $value) { 473 | $keys = array_keys($current, $value); 474 | 475 | foreach ($keys as $key) { 476 | unset($current[$key]); 477 | } 478 | } 479 | 480 | $this->attributes[$column] = array_values($current); 481 | 482 | $this->syncOriginalAttribute($column); 483 | } 484 | 485 | /** 486 | * Set the parent relation. 487 | * 488 | * @param \Illuminate\Database\Eloquent\Relations\Relation $relation 489 | */ 490 | public function setParentRelation(Relation $relation) 491 | { 492 | $this->parentRelation = $relation; 493 | } 494 | 495 | /** 496 | * Get the parent relation. 497 | * 498 | * @return \Illuminate\Database\Eloquent\Relations\Relation 499 | */ 500 | public function getParentRelation() 501 | { 502 | return $this->parentRelation; 503 | } 504 | 505 | /** 506 | * Create a new Eloquent query builder for the model. 507 | * 508 | * @param \sonvq\Cassandra\Query\Builder $query 509 | * @return \sonvq\Cassandra\Eloquent\Builder|static 510 | */ 511 | public function newEloquentBuilder($query) 512 | { 513 | return new Builder($query); 514 | } 515 | 516 | /** 517 | * Get a new query builder instance for the connection. 518 | * 519 | * @return Builder 520 | */ 521 | protected function newBaseQueryBuilder() 522 | { 523 | $connection = $this->getConnection(); 524 | 525 | return new QueryBuilder($connection, $connection->getPostProcessor()); 526 | } 527 | 528 | /** 529 | * Handle dynamic method calls into the method. 530 | * 531 | * @param string $method 532 | * @param array $parameters 533 | * @return mixed 534 | */ 535 | public function __call($method, $parameters) 536 | { 537 | // Unset method 538 | if ($method == 'unset') { 539 | return call_user_func_array([$this, 'drop'], $parameters); 540 | } 541 | 542 | return parent::__call($method, $parameters); 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel Cassandra 2 | =============== 3 | 4 | Content 5 | ------- 6 | 7 | Integrate Cassandra into your Laravel app, using Laravel's own API and methods. 8 | 9 | Table of contents 10 | ----------------- 11 | * [Installation](#installation) 12 | * [Configuration](#configuration) 13 | * [Eloquent](#eloquent) 14 | * [Optional: Alias](#optional-alias) 15 | * [Query Builder](#query-builder) 16 | * [Schema](#schema) 17 | * [Extensions](#extensions) 18 | * [Troubleshooting](#troubleshooting) 19 | * [Examples](#examples) 20 | 21 | Installation 22 | ------------ 23 | 24 | Make sure you have the Cassandra PHP driver installed. https://github.com/datastax/php-driver 25 | 26 | Installation using composer: 27 | 28 | ``` 29 | composer require sonvq/cassandra 30 | ``` 31 | 32 | ### Laravel version Compatibility 33 | 34 | Laravel | Package 35 | :---------|:---------- 36 | 5.2.x | 1.0.x 37 | 38 | 39 | Add Cassandra service provider in `config/app.php`: 40 | 41 | ```php 42 | sonvq\Cassandra\CassandraServiceProvider::class, 43 | ``` 44 | 45 | For usage with [Lumen](http://lumen.laravel.com), add the service provider in `bootstrap/app.php`. In this file, you will also need to enable Eloquent. You must however ensure that your call to `$app->withEloquent();` is **below** where you have registered the `CassandraServiceProvider`: 46 | 47 | ```php 48 | $app->register('sonvq\Cassandra\CassandraServiceProvider'); 49 | 50 | $app->withEloquent(); 51 | ``` 52 | 53 | The service provider will register a Cassandra database extension with the original database manager. There is no need to register additional facades or objects. When using cassandra connections, Laravel will automatically provide you with the corresponding cassandra objects. 54 | 55 | 56 | 57 | 58 | Configuration 59 | ------------- 60 | 61 | Change your default database connection name in `app/config/database.php`: 62 | 63 | ```php 64 | 'default' => env('DB_CONNECTION', 'cassandra'), 65 | ``` 66 | 67 | And add a new cassandra connection: 68 | 69 | ```php 70 | 'cassandra' => [ 71 | 'driver' => 'cassandra', 72 | 'host' => env('DB_HOST', 'localhost'), 73 | 'port' => env('DB_PORT', 27017), 74 | 'database' => env('DB_DATABASE', ''), 75 | 'username' => env('DB_USERNAME', ''), 76 | 'password' => env('DB_PASSWORD', ''), 77 | 'options' => [ 78 | 'db' => 'admin' // sets the authentication database required by cassandra 3 79 | ] 80 | ], 81 | ``` 82 | 83 | You can connect to multiple servers or replica sets with the following configuration: 84 | 85 | ```php 86 | 'cassandra' => [ 87 | 'driver' => 'cassandra', 88 | 'host' => ['server1', 'server2'], 89 | 'port' => env('DB_PORT', 27017), 90 | 'database' => env('DB_DATABASE', ''), 91 | 'username' => env('DB_USERNAME', ''), 92 | 'password' => env('DB_PASSWORD', ''), 93 | 'options' => ['replicaSet' => 'replicaSetName'] 94 | ], 95 | ``` 96 | 97 | Eloquent 98 | -------- 99 | 100 | This package includes a Cassandra enabled Eloquent class that you can use to define models for corresponding collections. 101 | 102 | ```php 103 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 104 | 105 | class User extends Eloquent {} 106 | ``` 107 | 108 | Note that we did not tell Eloquent which collection to use for the `User` model. Just like the original Eloquent, the lower-case, plural name of the class will be used as the table name unless another name is explicitly specified. You may specify a custom collection (alias for table) by defining a `collection` property on your model: 109 | 110 | ```php 111 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 112 | 113 | class User extends Eloquent { 114 | 115 | protected $collection = 'users_collection'; 116 | 117 | } 118 | ``` 119 | 120 | **NOTE:** Eloquent will also assume that each collection has a primary key column named id. You may define a `primaryKey` property to override this convention. Likewise, you may define a `connection` property to override the name of the database connection that should be used when utilizing the model. 121 | 122 | ```php 123 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 124 | 125 | class MyModel extends Eloquent { 126 | 127 | protected $connection = 'cassandra'; 128 | 129 | } 130 | ``` 131 | 132 | Everything else (should) work just like the original Eloquent model. Read more about the Eloquent on http://laravel.com/docs/eloquent 133 | 134 | ### Optional: Alias 135 | 136 | You may also register an alias for the Cassandra model by adding the following to the alias array in `app/config/app.php`: 137 | 138 | ```php 139 | 'Moloquent' => 'sonvq\Cassandra\Eloquent\Model', 140 | ``` 141 | 142 | This will allow you to use the registered alias like: 143 | 144 | ```php 145 | class MyModel extends Moloquent {} 146 | ``` 147 | 148 | Query Builder 149 | ------------- 150 | 151 | The database driver plugs right into the original query builder. When using cassandra connections, you will be able to build fluent queries to perform database operations. For your convenience, there is a `collection` alias for `table` as well as some additional cassandra specific operators/operations. 152 | 153 | ```php 154 | $users = DB::collection('users')->get(); 155 | 156 | $user = DB::collection('users')->where('name', 'John')->first(); 157 | ``` 158 | 159 | If you did not change your default database connection, you will need to specify it when querying. 160 | 161 | ```php 162 | $user = DB::connection('cassandra')->collection('users')->get(); 163 | ``` 164 | 165 | Read more about the query builder on http://laravel.com/docs/queries 166 | 167 | Schema 168 | ------ 169 | 170 | The database driver also has (limited) schema builder support. You can easily manipulate collections and set indexes: 171 | 172 | ```php 173 | Schema::create('users', function($collection) 174 | { 175 | $collection->index('name'); 176 | 177 | $collection->unique('email'); 178 | }); 179 | ``` 180 | 181 | Supported operations are: 182 | 183 | - create and drop 184 | - collection 185 | - hasCollection 186 | - index and dropIndex (compound indexes supported as well) 187 | - unique 188 | - background, sparse, expire (Cassandra specific) 189 | 190 | All other (unsupported) operations are implemented as dummy pass-through methods, because Cassandra does not use a predefined schema. Read more about the schema builder on http://laravel.com/docs/schema 191 | 192 | Extensions 193 | ---------- 194 | 195 | ### Auth 196 | 197 | If you want to use Laravel's native Auth functionality, register this included service provider: 198 | 199 | ```php 200 | 'sonvq\Cassandra\Auth\PasswordResetServiceProvider', 201 | ``` 202 | 203 | This service provider will slightly modify the internal DatabaseReminderRepository to add support for Cassandra based password reminders. If you don't use password reminders, you don't have to register this service provider and everything else should work just fine. 204 | 205 | ### Queues 206 | 207 | If you want to use Cassandra as your database backend, change the the driver in `config/queue.php`: 208 | 209 | ```php 210 | 'connections' => [ 211 | 'database' => [ 212 | 'driver' => 'cassandra', 213 | 'table' => 'jobs', 214 | 'queue' => 'default', 215 | 'expire' => 60, 216 | ], 217 | ``` 218 | 219 | Examples 220 | -------- 221 | 222 | ### Basic Usage 223 | 224 | **Retrieving All Models** 225 | 226 | ```php 227 | $users = User::all(); 228 | ``` 229 | 230 | **Retrieving A Record By Primary Key** 231 | 232 | ```php 233 | $user = User::find('517c43667db388101e00000f'); 234 | ``` 235 | 236 | **Wheres** 237 | 238 | ```php 239 | $users = User::where('votes', '>', 100)->take(10)->get(); 240 | ``` 241 | 242 | **Or Statements** 243 | 244 | ```php 245 | $users = User::where('votes', '>', 100)->orWhere('name', 'John')->get(); 246 | ``` 247 | 248 | **And Statements** 249 | 250 | ```php 251 | $users = User::where('votes', '>', 100)->where('name', '=', 'John')->get(); 252 | ``` 253 | 254 | **Using Where In With An Array** 255 | 256 | ```php 257 | $users = User::whereIn('age', [16, 18, 20])->get(); 258 | ``` 259 | 260 | When using `whereNotIn` objects will be returned if the field is non existent. Combine with `whereNotNull('age')` to leave out those documents. 261 | 262 | **Using Where Between** 263 | 264 | ```php 265 | $users = User::whereBetween('votes', [1, 100])->get(); 266 | ``` 267 | 268 | **Where null** 269 | 270 | ```php 271 | $users = User::whereNull('updated_at')->get(); 272 | ``` 273 | 274 | **Order By** 275 | 276 | ```php 277 | $users = User::orderBy('name', 'desc')->get(); 278 | ``` 279 | 280 | **Offset & Limit** 281 | 282 | ```php 283 | $users = User::skip(10)->take(5)->get(); 284 | ``` 285 | 286 | **Distinct** 287 | 288 | Distinct requires a field for which to return the distinct values. 289 | 290 | ```php 291 | $users = User::distinct()->get(['name']); 292 | // or 293 | $users = User::distinct('name')->get(); 294 | ``` 295 | 296 | Distinct can be combined with **where**: 297 | 298 | ```php 299 | $users = User::where('active', true)->distinct('name')->get(); 300 | ``` 301 | 302 | **Advanced Wheres** 303 | 304 | ```php 305 | $users = User::where('name', '=', 'John')->orWhere(function($query) 306 | { 307 | $query->where('votes', '>', 100) 308 | ->where('title', '<>', 'Admin'); 309 | }) 310 | ->get(); 311 | ``` 312 | 313 | **Group By** 314 | 315 | Selected columns that are not grouped will be aggregated with the $last function. 316 | 317 | ```php 318 | $users = Users::groupBy('title')->get(['title', 'name']); 319 | ``` 320 | 321 | **Aggregation** 322 | 323 | *Aggregations are only available for Cassandra versions greater than 2.2.* 324 | 325 | ```php 326 | $total = Order::count(); 327 | $price = Order::max('price'); 328 | $price = Order::min('price'); 329 | $price = Order::avg('price'); 330 | $total = Order::sum('price'); 331 | ``` 332 | 333 | Aggregations can be combined with **where**: 334 | 335 | ```php 336 | $sold = Orders::where('sold', true)->sum('price'); 337 | ``` 338 | 339 | **Like** 340 | 341 | ```php 342 | $user = Comment::where('body', 'like', '%spam%')->get(); 343 | ``` 344 | 345 | **Incrementing or decrementing a value of a column** 346 | 347 | Perform increments or decrements (default 1) on specified attributes: 348 | 349 | ```php 350 | User::where('name', 'John Doe')->increment('age'); 351 | User::where('name', 'Jaques')->decrement('weight', 50); 352 | ``` 353 | 354 | The number of updated objects is returned: 355 | 356 | ```php 357 | $count = User->increment('age'); 358 | ``` 359 | 360 | You may also specify additional columns to update: 361 | 362 | ```php 363 | User::where('age', '29')->increment('age', 1, ['group' => 'thirty something']); 364 | User::where('bmi', 30)->decrement('bmi', 1, ['category' => 'overweight']); 365 | ``` 366 | 367 | **Soft deleting** 368 | 369 | When soft deleting a model, it is not actually removed from your database. Instead, a deleted_at timestamp is set on the record. To enable soft deletes for a model, apply the SoftDeletingTrait to the model: 370 | 371 | ```php 372 | use sonvq\Cassandra\Eloquent\SoftDeletes; 373 | 374 | class User extends Eloquent { 375 | 376 | use SoftDeletes; 377 | 378 | protected $dates = ['deleted_at']; 379 | 380 | } 381 | ``` 382 | 383 | For more information check http://laravel.com/docs/eloquent#soft-deleting 384 | 385 | ### Cassandra specific operators 386 | 387 | **Exists** 388 | 389 | Matches documents that have the specified field. 390 | 391 | ```php 392 | User::where('age', 'exists', true)->get(); 393 | ``` 394 | 395 | **All** 396 | 397 | Matches arrays that contain all elements specified in the query. 398 | 399 | ```php 400 | User::where('roles', 'all', ['moderator', 'author'])->get(); 401 | ``` 402 | 403 | **Size** 404 | 405 | Selects documents if the array field is a specified size. 406 | 407 | ```php 408 | User::where('tags', 'size', 3)->get(); 409 | ``` 410 | 411 | **Regex** 412 | 413 | Selects documents where values match a specified regular expression. 414 | 415 | ```php 416 | User::where('name', 'regex', new CassandraRegex("/.*doe/i"))->get(); 417 | ``` 418 | 419 | **NOTE:** you can also use the Laravel regexp operations. These are a bit more flexible and will automatically convert your regular expression string to a CassandraRegex object. 420 | 421 | ```php 422 | User::where('name', 'regexp', '/.*doe/i'))->get(); 423 | ``` 424 | 425 | And the inverse: 426 | 427 | ```php 428 | User::where('name', 'not regexp', '/.*doe/i'))->get(); 429 | ``` 430 | 431 | **Type** 432 | 433 | Selects documents if a field is of the specified type. For more information check: http://docs.cassandra.org/manual/reference/operator/query/type/#op._S_type 434 | 435 | ```php 436 | User::where('age', 'type', 2)->get(); 437 | ``` 438 | 439 | **Mod** 440 | 441 | Performs a modulo operation on the value of a field and selects documents with a specified result. 442 | 443 | ```php 444 | User::where('age', 'mod', [10, 0])->get(); 445 | ``` 446 | 447 | **Where** 448 | 449 | Matches documents that satisfy a JavaScript expression. For more information check http://docs.cassandra.org/manual/reference/operator/query/where/#op._S_where 450 | 451 | ### Inserts, updates and deletes 452 | 453 | Inserting, updating and deleting records works just like the original Eloquent. 454 | 455 | **Saving a new model** 456 | 457 | ```php 458 | $user = new User; 459 | $user->name = 'John'; 460 | $user->save(); 461 | ``` 462 | 463 | You may also use the create method to save a new model in a single line: 464 | 465 | ```php 466 | User::create(['name' => 'John']); 467 | ``` 468 | 469 | **Updating a model** 470 | 471 | To update a model, you may retrieve it, change an attribute, and use the save method. 472 | 473 | ```php 474 | $user = User::first(); 475 | $user->email = 'john@foo.com'; 476 | $user->save(); 477 | ``` 478 | 479 | *There is also support for upsert operations, check https://github.com/sonvq/laravel-cassandra#cassandra-specific-operations* 480 | 481 | **Deleting a model** 482 | 483 | To delete a model, simply call the delete method on the instance: 484 | 485 | ```php 486 | $user = User::first(); 487 | $user->delete(); 488 | ``` 489 | 490 | Or deleting a model by its key: 491 | 492 | ```php 493 | User::destroy('517c43667db388101e00000f'); 494 | ``` 495 | 496 | For more information about model manipulation, check http://laravel.com/docs/eloquent#insert-update-delete 497 | 498 | ### Dates 499 | 500 | Eloquent allows you to work with Carbon/DateTime objects instead of CassandraDate objects. Internally, these dates will be converted to CassandraDate objects when saved to the database. If you wish to use this functionality on non-default date fields you will need to manually specify them as described here: http://laravel.com/docs/eloquent#date-mutators 501 | 502 | Example: 503 | 504 | ```php 505 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 506 | 507 | class User extends Eloquent { 508 | 509 | protected $dates = ['birthday']; 510 | 511 | } 512 | ``` 513 | 514 | Which allows you to execute queries like: 515 | 516 | ```php 517 | $users = User::where('birthday', '>', new DateTime('-18 years'))->get(); 518 | ``` 519 | 520 | ### Relations 521 | 522 | Supported relations are: 523 | 524 | - hasOne 525 | - hasMany 526 | - belongsTo 527 | - belongsToMany 528 | - embedsOne 529 | - embedsMany 530 | 531 | Example: 532 | 533 | ```php 534 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 535 | 536 | class User extends Eloquent { 537 | 538 | public function items() 539 | { 540 | return $this->hasMany('Item'); 541 | } 542 | 543 | } 544 | ``` 545 | 546 | And the inverse relation: 547 | 548 | ```php 549 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 550 | 551 | class Item extends Eloquent { 552 | 553 | public function user() 554 | { 555 | return $this->belongsTo('User'); 556 | } 557 | 558 | } 559 | ``` 560 | 561 | The belongsToMany relation will not use a pivot "table", but will push id's to a __related_ids__ attribute instead. This makes the second parameter for the belongsToMany method useless. If you want to define custom keys for your relation, set it to `null`: 562 | 563 | ```php 564 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 565 | 566 | class User extends Eloquent { 567 | 568 | public function groups() 569 | { 570 | return $this->belongsToMany('Group', null, 'users', 'groups'); 571 | } 572 | 573 | } 574 | ``` 575 | 576 | 577 | Other relations are not yet supported, but may be added in the future. Read more about these relations on http://laravel.com/docs/eloquent#relationships 578 | 579 | ### EmbedsMany Relations 580 | 581 | If you want to embed models, rather than referencing them, you can use the `embedsMany` relation. This relation is similar to the `hasMany` relation, but embeds the models inside the parent object. 582 | 583 | **REMEMBER**: these relations return Eloquent collections, they don't return query builder objects! 584 | 585 | ```php 586 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 587 | 588 | class User extends Eloquent { 589 | 590 | public function books() 591 | { 592 | return $this->embedsMany('Book'); 593 | } 594 | 595 | } 596 | ``` 597 | 598 | You access the embedded models through the dynamic property: 599 | 600 | ```php 601 | $books = User::first()->books; 602 | ``` 603 | 604 | The inverse relation is auto*magically* available, you don't need to define this reverse relation. 605 | 606 | ```php 607 | $user = $book->user; 608 | ``` 609 | 610 | Inserting and updating embedded models works similar to the `hasMany` relation: 611 | 612 | ```php 613 | $book = new Book(['title' => 'A Game of Thrones']); 614 | 615 | $user = User::first(); 616 | 617 | $book = $user->books()->save($book); 618 | // or 619 | $book = $user->books()->create(['title' => 'A Game of Thrones']) 620 | ``` 621 | 622 | You can update embedded models using their `save` method (available since release 2.0.0): 623 | 624 | ```php 625 | $book = $user->books()->first(); 626 | 627 | $book->title = 'A Game of Thrones'; 628 | 629 | $book->save(); 630 | ``` 631 | 632 | You can remove an embedded model by using the `destroy` method on the relation, or the `delete` method on the model (available since release 2.0.0): 633 | 634 | ```php 635 | $book = $user->books()->first(); 636 | 637 | $book->delete(); 638 | // or 639 | $user->books()->destroy($book); 640 | ``` 641 | 642 | If you want to add or remove an embedded model, without touching the database, you can use the `associate` and `dissociate` methods. To eventually write the changes to the database, save the parent object: 643 | 644 | ```php 645 | $user->books()->associate($book); 646 | 647 | $user->save(); 648 | ``` 649 | 650 | Like other relations, embedsMany assumes the local key of the relationship based on the model name. You can override the default local key by passing a second argument to the embedsMany method: 651 | 652 | ```php 653 | return $this->embedsMany('Book', 'local_key'); 654 | ``` 655 | 656 | Embedded relations will return a Collection of embedded items instead of a query builder. Check out the available operations here: https://laravel.com/docs/master/collections 657 | 658 | ### EmbedsOne Relations 659 | 660 | The embedsOne relation is similar to the EmbedsMany relation, but only embeds a single model. 661 | 662 | ```php 663 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 664 | 665 | class Book extends Eloquent { 666 | 667 | public function author() 668 | { 669 | return $this->embedsOne('Author'); 670 | } 671 | 672 | } 673 | ``` 674 | 675 | You access the embedded models through the dynamic property: 676 | 677 | ```php 678 | $author = Book::first()->author; 679 | ``` 680 | 681 | Inserting and updating embedded models works similar to the `hasOne` relation: 682 | 683 | ```php 684 | $author = new Author(['name' => 'John Doe']); 685 | 686 | $book = Books::first(); 687 | 688 | $author = $book->author()->save($author); 689 | // or 690 | $author = $book->author()->create(['name' => 'John Doe']); 691 | ``` 692 | 693 | You can update the embedded model using the `save` method (available since release 2.0.0): 694 | 695 | ```php 696 | $author = $book->author; 697 | 698 | $author->name = 'Jane Doe'; 699 | $author->save(); 700 | ``` 701 | 702 | You can replace the embedded model with a new model like this: 703 | 704 | ```php 705 | $newAuthor = new Author(['name' => 'Jane Doe']); 706 | $book->author()->save($newAuthor); 707 | ``` 708 | 709 | ### MySQL Relations 710 | 711 | If you're using a hybrid Cassandra and SQL setup, you're in luck! The model will automatically return a Cassandra- or SQL-relation based on the type of the related model. Of course, if you want this functionality to work both ways, your SQL-models will need use the `sonvq\Cassandra\Eloquent\HybridRelations` trait. Note that this functionality only works for hasOne, hasMany and belongsTo relations. 712 | 713 | Example SQL-based User model: 714 | 715 | ```php 716 | use sonvq\Cassandra\Eloquent\HybridRelations; 717 | 718 | class User extends Eloquent { 719 | 720 | use HybridRelations; 721 | 722 | protected $connection = 'mysql'; 723 | 724 | public function messages() 725 | { 726 | return $this->hasMany('Message'); 727 | } 728 | 729 | } 730 | ``` 731 | 732 | And the Cassandra-based Message model: 733 | 734 | ```php 735 | use sonvq\Cassandra\Eloquent\Model as Eloquent; 736 | 737 | class Message extends Eloquent { 738 | 739 | protected $connection = 'cassandra'; 740 | 741 | public function user() 742 | { 743 | return $this->belongsTo('User'); 744 | } 745 | 746 | } 747 | ``` 748 | 749 | ### Raw Expressions 750 | 751 | These expressions will be injected directly into the query. 752 | 753 | ```php 754 | User::whereRaw(['age' => array('$gt' => 30, '$lt' => 40]))->get(); 755 | ``` 756 | 757 | You can also perform raw expressions on the internal CassandraCollection object. If this is executed on the model class, it will return a collection of models. If this is executed on the query builder, it will return the original response. 758 | 759 | ```php 760 | // Returns a collection of User models. 761 | $models = User::raw(function($collection) 762 | { 763 | return $collection->find(); 764 | }); 765 | 766 | // Returns the original CassandraCursor. 767 | $cursor = DB::collection('users')->raw(function($collection) 768 | { 769 | return $collection->find(); 770 | }); 771 | ``` 772 | 773 | Optional: if you don't pass a closure to the raw method, the internal CassandraCollection object will be accessible: 774 | 775 | ```php 776 | $model = User::raw()->findOne(['age' => array('$lt' => 18])); 777 | ``` 778 | 779 | The internal CassandraClient and Cassandra objects can be accessed like this: 780 | 781 | ```php 782 | $client = DB::getCassandraClient(); 783 | $db = DB::getCassandra(); 784 | ``` 785 | 786 | ### Cassandra specific operations 787 | 788 | **Cursor timeout** 789 | 790 | To prevent CassandraCursorTimeout exceptions, you can manually set a timeout value that will be applied to the cursor: 791 | 792 | ```php 793 | DB::collection('users')->timeout(-1)->get(); 794 | ``` 795 | 796 | **Upsert** 797 | 798 | Update or insert a document. Additional options for the update method are passed directly to the native update method. 799 | 800 | ```php 801 | DB::collection('users')->where('name', 'John') 802 | ->update($data, ['upsert' => true]); 803 | ``` 804 | 805 | **Projections** 806 | 807 | You can apply projections to your queries using the `project` method. 808 | 809 | ```php 810 | DB::collection('items')->project(['tags' => array('$slice' => 1]))->get(); 811 | ``` 812 | 813 | **Projections with Pagination** 814 | 815 | ```php 816 | $limit = 25; 817 | $projections = ['id', 'name']; 818 | DB::collection('items')->paginate($limit, $projections); 819 | ``` 820 | 821 | 822 | **Push** 823 | 824 | Add an items to an array. 825 | 826 | ```php 827 | DB::collection('users')->where('name', 'John')->push('items', 'boots'); 828 | DB::collection('users')->where('name', 'John')->push('messages', ['from' => 'Jane Doe', 'message' => 'Hi John']); 829 | ``` 830 | 831 | If you don't want duplicate items, set the third parameter to `true`: 832 | 833 | ```php 834 | DB::collection('users')->where('name', 'John')->push('items', 'boots', true); 835 | ``` 836 | 837 | **Pull** 838 | 839 | Remove an item from an array. 840 | 841 | ```php 842 | DB::collection('users')->where('name', 'John')->pull('items', 'boots'); 843 | DB::collection('users')->where('name', 'John')->pull('messages', ['from' => 'Jane Doe', 'message' => 'Hi John']); 844 | ``` 845 | 846 | **Unset** 847 | 848 | Remove one or more fields from a document. 849 | 850 | ```php 851 | DB::collection('users')->where('name', 'John')->unset('note'); 852 | ``` 853 | 854 | You can also perform an unset on a model. 855 | 856 | ```php 857 | $user = User::where('name', 'John')->first(); 858 | $user->unset('note'); 859 | ``` 860 | 861 | ### Query Caching 862 | 863 | You may easily cache the results of a query using the remember method: 864 | 865 | ```php 866 | $users = User::remember(10)->get(); 867 | ``` 868 | 869 | *From: http://laravel.com/docs/queries#caching-queries* 870 | 871 | ### Query Logging 872 | 873 | By default, Laravel keeps a log in memory of all queries that have been run for the current request. However, in some cases, such as when inserting a large number of rows, this can cause the application to use excess memory. To disable the log, you may use the `disableQueryLog` method: 874 | 875 | ```php 876 | DB::connection()->disableQueryLog(); 877 | ``` 878 | 879 | *From: http://laravel.com/docs/database#query-logging* 880 | -------------------------------------------------------------------------------- /src/sonvq/Cassandra/Query/Builder.php: -------------------------------------------------------------------------------- 1 | ', '<=', '>=', '<>', '!=', 60 | 'like', 'not like', 'between', 'ilike', 61 | '&', '|', '^', '<<', '>>', 62 | 'rlike', 'regexp', 'not regexp', 63 | 'exists', 'type', 'mod', 'where', 'all', 'size', 'regex', 'text', 'slice', 'elemmatch', 64 | 'geowithin', 'geointersects', 'near', 'nearsphere', 'geometry', 65 | 'maxdistance', 'center', 'centersphere', 'box', 'polygon', 'uniquedocs', 66 | ]; 67 | 68 | /** 69 | * Operator conversion. 70 | * 71 | * @var array 72 | */ 73 | protected $conversion = [ 74 | '=' => '=', 75 | '!=' => '$ne', 76 | '<>' => '$ne', 77 | '<' => '$lt', 78 | '<=' => '$lte', 79 | '>' => '$gt', 80 | '>=' => '$gte', 81 | ]; 82 | 83 | /** 84 | * Create a new query builder instance. 85 | * 86 | * @param Connection $connection 87 | * @param Processor $processor 88 | */ 89 | public function __construct(Connection $connection, Processor $processor) 90 | { 91 | $this->grammar = new Grammar; 92 | $this->connection = $connection; 93 | $this->processor = $processor; 94 | } 95 | 96 | /** 97 | * Set the projections. 98 | * 99 | * @param array $columns 100 | * @return $this 101 | */ 102 | public function project($columns) 103 | { 104 | $this->projections = is_array($columns) ? $columns : func_get_args(); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Set the cursor timeout in seconds. 111 | * 112 | * @param int $seconds 113 | * @return $this 114 | */ 115 | public function timeout($seconds) 116 | { 117 | $this->timeout = $seconds; 118 | 119 | return $this; 120 | } 121 | 122 | /** 123 | * Set the cursor hint. 124 | * 125 | * @param mixed $index 126 | * @return $this 127 | */ 128 | public function hint($index) 129 | { 130 | $this->hint = $index; 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Execute a query for a single record by ID. 137 | * 138 | * @param mixed $id 139 | * @param array $columns 140 | * @return mixed 141 | */ 142 | public function find($id, $columns = []) 143 | { 144 | return $this->where('_id', '=', $this->convertKey($id))->first($columns); 145 | } 146 | 147 | /** 148 | * Execute the query as a "select" statement. 149 | * 150 | * @param array $columns 151 | * @return array|static[] 152 | */ 153 | public function get($columns = []) 154 | { 155 | return $this->getFresh($columns); 156 | } 157 | 158 | /** 159 | * Execute the query as a fresh "select" statement. 160 | * 161 | * @param array $columns 162 | * @return array|static[] 163 | */ 164 | public function getFresh($columns = []) 165 | { 166 | // If no columns have been specified for the select statement, we will set them 167 | // here to either the passed columns, or the standard default of retrieving 168 | // all of the columns on the table using the "wildcard" column character. 169 | if (is_null($this->columns)) { 170 | $this->columns = $columns; 171 | } 172 | 173 | // Drop all columns if * is present, Cassandra does not work this way. 174 | if (in_array('*', $this->columns)) { 175 | $this->columns = []; 176 | } 177 | 178 | // Compile wheres 179 | $wheres = $this->compileWheres(); 180 | 181 | // Use Cassandra's aggregation framework when using grouping or aggregation functions. 182 | if ($this->groups or $this->aggregate or $this->paginating) { 183 | $group = []; 184 | 185 | // Add grouping columns to the $group part of the aggregation pipeline. 186 | if ($this->groups) { 187 | foreach ($this->groups as $column) { 188 | $group['_id'][$column] = '$' . $column; 189 | 190 | // When grouping, also add the $last operator to each grouped field, 191 | // this mimics MySQL's behaviour a bit. 192 | $group[$column] = ['$last' => '$' . $column]; 193 | } 194 | 195 | // Do the same for other columns that are selected. 196 | foreach ($this->columns as $column) { 197 | $key = str_replace('.', '_', $column); 198 | 199 | $group[$key] = ['$last' => '$' . $column]; 200 | } 201 | } 202 | 203 | // Add aggregation functions to the $group part of the aggregation pipeline, 204 | // these may override previous aggregations. 205 | if ($this->aggregate) { 206 | $function = $this->aggregate['function']; 207 | 208 | foreach ($this->aggregate['columns'] as $column) { 209 | // Translate count into sum. 210 | if ($function == 'count') { 211 | $group['aggregate'] = ['$sum' => 1]; 212 | } // Pass other functions directly. 213 | else { 214 | $group['aggregate'] = ['$' . $function => '$' . $column]; 215 | } 216 | } 217 | } 218 | 219 | // When using pagination, we limit the number of returned columns 220 | // by adding a projection. 221 | if ($this->paginating) { 222 | foreach ($this->columns as $column) { 223 | $this->projections[$column] = 1; 224 | } 225 | } 226 | 227 | // The _id field is mandatory when using grouping. 228 | if ($group and empty($group['_id'])) { 229 | $group['_id'] = null; 230 | } 231 | 232 | // Build the aggregation pipeline. 233 | $pipeline = []; 234 | if ($wheres) { 235 | $pipeline[] = ['$match' => $wheres]; 236 | } 237 | if ($group) { 238 | $pipeline[] = ['$group' => $group]; 239 | } 240 | 241 | // Apply order and limit 242 | if ($this->orders) { 243 | $pipeline[] = ['$sort' => $this->orders]; 244 | } 245 | if ($this->offset) { 246 | $pipeline[] = ['$skip' => $this->offset]; 247 | } 248 | if ($this->limit) { 249 | $pipeline[] = ['$limit' => $this->limit]; 250 | } 251 | if ($this->projections) { 252 | $pipeline[] = ['$project' => $this->projections]; 253 | } 254 | 255 | $options = [ 256 | 'typeMap' => ['root' => 'array', 'document' => 'array'], 257 | ]; 258 | 259 | // Execute aggregation 260 | $results = iterator_to_array($this->collection->aggregate($pipeline, $options)); 261 | 262 | // Return results 263 | return $results; 264 | } // Distinct query 265 | elseif ($this->distinct) { 266 | // Return distinct results directly 267 | $column = isset($this->columns[0]) ? $this->columns[0] : '_id'; 268 | 269 | // Execute distinct 270 | if ($wheres) { 271 | $result = $this->collection->distinct($column, $wheres); 272 | } else { 273 | $result = $this->collection->distinct($column); 274 | } 275 | 276 | return $result; 277 | } // Normal query 278 | else { 279 | $columns = []; 280 | 281 | // Convert select columns to simple projections. 282 | foreach ($this->columns as $column) { 283 | $columns[$column] = true; 284 | } 285 | 286 | // Add custom projections. 287 | if ($this->projections) { 288 | $columns = array_merge($columns, $this->projections); 289 | } 290 | $options = []; 291 | 292 | // Apply order, offset, limit and projection 293 | if ($this->timeout) { 294 | $options['maxTimeMS'] = $this->timeout; 295 | } 296 | if ($this->orders) { 297 | $options['sort'] = $this->orders; 298 | } 299 | if ($this->offset) { 300 | $options['skip'] = $this->offset; 301 | } 302 | if ($this->limit) { 303 | $options['limit'] = $this->limit; 304 | } 305 | if ($columns) { 306 | $options['projection'] = $columns; 307 | } 308 | // if ($this->hint) $cursor->hint($this->hint); 309 | 310 | // Fix for legacy support, converts the results to arrays instead of objects. 311 | $options['typeMap'] = ['root' => 'array', 'document' => 'array']; 312 | 313 | // Execute query and get CassandraCursor 314 | $cursor = $this->collection->find($wheres, $options); 315 | 316 | // Return results as an array with numeric keys 317 | return iterator_to_array($cursor, false); 318 | } 319 | } 320 | 321 | /** 322 | * Generate the unique cache key for the current query. 323 | * 324 | * @return string 325 | */ 326 | public function generateCacheKey() 327 | { 328 | $key = [ 329 | 'connection' => $this->collection->getDatabaseName(), 330 | 'collection' => $this->collection->getCollectionName(), 331 | 'wheres' => $this->wheres, 332 | 'columns' => $this->columns, 333 | 'groups' => $this->groups, 334 | 'orders' => $this->orders, 335 | 'offset' => $this->offset, 336 | 'limit' => $this->limit, 337 | 'aggregate' => $this->aggregate, 338 | ]; 339 | 340 | return md5(serialize(array_values($key))); 341 | } 342 | 343 | /** 344 | * Execute an aggregate function on the database. 345 | * 346 | * @param string $function 347 | * @param array $columns 348 | * @return mixed 349 | */ 350 | public function aggregate($function, $columns = []) 351 | { 352 | $this->aggregate = compact('function', 'columns'); 353 | 354 | $results = $this->get($columns); 355 | 356 | // Once we have executed the query, we will reset the aggregate property so 357 | // that more select queries can be executed against the database without 358 | // the aggregate value getting in the way when the grammar builds it. 359 | $this->columns = null; 360 | $this->aggregate = null; 361 | 362 | if (isset($results[0])) { 363 | $result = (array) $results[0]; 364 | 365 | return $result['aggregate']; 366 | } 367 | } 368 | 369 | /** 370 | * Determine if any rows exist for the current query. 371 | * 372 | * @return bool 373 | */ 374 | public function exists() 375 | { 376 | return ! is_null($this->first()); 377 | } 378 | 379 | /** 380 | * Force the query to only return distinct results. 381 | * 382 | * @return Builder 383 | */ 384 | public function distinct($column = false) 385 | { 386 | $this->distinct = true; 387 | 388 | if ($column) { 389 | $this->columns = [$column]; 390 | } 391 | 392 | return $this; 393 | } 394 | 395 | /** 396 | * Add an "order by" clause to the query. 397 | * 398 | * @param string $column 399 | * @param string $direction 400 | * @return Builder 401 | */ 402 | public function orderBy($column, $direction = 'asc') 403 | { 404 | if (is_string($direction)) { 405 | $direction = (strtolower($direction) == 'asc' ? 1 : -1); 406 | } 407 | 408 | if ($column == 'natural') { 409 | $this->orders['$natural'] = $direction; 410 | } else { 411 | $this->orders[$column] = $direction; 412 | } 413 | 414 | return $this; 415 | } 416 | 417 | /** 418 | * Add a where between statement to the query. 419 | * 420 | * @param string $column 421 | * @param array $values 422 | * @param string $boolean 423 | * @param bool $not 424 | * @return Builder 425 | */ 426 | public function whereBetween($column, array $values, $boolean = 'and', $not = false) 427 | { 428 | $type = 'between'; 429 | 430 | $this->wheres[] = compact('column', 'type', 'boolean', 'values', 'not'); 431 | 432 | return $this; 433 | } 434 | 435 | /** 436 | * Set the limit and offset for a given page. 437 | * 438 | * @param int $page 439 | * @param int $perPage 440 | * @return \Illuminate\Database\Query\Builder|static 441 | */ 442 | public function forPage($page, $perPage = 15) 443 | { 444 | $this->paginating = true; 445 | 446 | return $this->skip(($page - 1) * $perPage)->take($perPage); 447 | } 448 | 449 | /** 450 | * Insert a new record into the database. 451 | * 452 | * @param array $values 453 | * @return bool 454 | */ 455 | public function insert(array $values) 456 | { 457 | // Since every insert gets treated like a batch insert, we will have to detect 458 | // if the user is inserting a single document or an array of documents. 459 | $batch = true; 460 | 461 | foreach ($values as $value) { 462 | // As soon as we find a value that is not an array we assume the user is 463 | // inserting a single document. 464 | if (! is_array($value)) { 465 | $batch = false; 466 | break; 467 | } 468 | } 469 | 470 | if (! $batch) { 471 | $values = [$values]; 472 | } 473 | 474 | // Batch insert 475 | $result = $this->collection->insertMany($values); 476 | 477 | return (1 == (int) $result->isAcknowledged()); 478 | } 479 | 480 | /** 481 | * Insert a new record and get the value of the primary key. 482 | * 483 | * @param array $values 484 | * @param string $sequence 485 | * @return int 486 | */ 487 | public function insertGetId(array $values, $sequence = null) 488 | { 489 | $result = $this->collection->insertOne($values); 490 | 491 | if (1 == (int) $result->isAcknowledged()) { 492 | if (is_null($sequence)) { 493 | $sequence = '_id'; 494 | } 495 | 496 | // Return id 497 | return $sequence == '_id' ? $result->getInsertedId() : $values[$sequence]; 498 | } 499 | } 500 | 501 | /** 502 | * Update a record in the database. 503 | * 504 | * @param array $values 505 | * @param array $options 506 | * @return int 507 | */ 508 | public function update(array $values, array $options = []) 509 | { 510 | // Use $set as default operator. 511 | if (! starts_with(key($values), '$')) { 512 | $values = ['$set' => $values]; 513 | } 514 | 515 | return $this->performUpdate($values, $options); 516 | } 517 | 518 | /** 519 | * Increment a column's value by a given amount. 520 | * 521 | * @param string $column 522 | * @param int $amount 523 | * @param array $extra 524 | * @return int 525 | */ 526 | public function increment($column, $amount = 1, array $extra = [], array $options = []) 527 | { 528 | $query = ['$inc' => [$column => $amount]]; 529 | 530 | if (! empty($extra)) { 531 | $query['$set'] = $extra; 532 | } 533 | 534 | // Protect 535 | $this->where(function ($query) use ($column) { 536 | $query->where($column, 'exists', false); 537 | 538 | $query->orWhereNotNull($column); 539 | }); 540 | 541 | return $this->performUpdate($query, $options); 542 | } 543 | 544 | /** 545 | * Decrement a column's value by a given amount. 546 | * 547 | * @param string $column 548 | * @param int $amount 549 | * @param array $extra 550 | * @return int 551 | */ 552 | public function decrement($column, $amount = 1, array $extra = [], array $options = []) 553 | { 554 | return $this->increment($column, -1 * $amount, $extra, $options); 555 | } 556 | 557 | /** 558 | * Get an array with the values of a given column. 559 | * 560 | * @param string $column 561 | * @param string|null $key 562 | * @return array 563 | */ 564 | public function pluck($column, $key = null) 565 | { 566 | $results = $this->get(is_null($key) ? [$column] : [$column, $key]); 567 | 568 | // If the columns are qualified with a table or have an alias, we cannot use 569 | // those directly in the "pluck" operations since the results from the DB 570 | // are only keyed by the column itself. We'll strip the table out here. 571 | return Arr::pluck( 572 | $results, 573 | $column, 574 | $key 575 | ); 576 | } 577 | 578 | /** 579 | * Delete a record from the database. 580 | * 581 | * @param mixed $id 582 | * @return int 583 | */ 584 | public function delete($id = null) 585 | { 586 | $wheres = $this->compileWheres(); 587 | $result = $this->collection->DeleteMany($wheres); 588 | if (1 == (int) $result->isAcknowledged()) { 589 | return $result->getDeletedCount(); 590 | } 591 | 592 | return 0; 593 | } 594 | 595 | /** 596 | * Set the collection which the query is targeting. 597 | * 598 | * @param string $collection 599 | * @return Builder 600 | */ 601 | public function from($collection) 602 | { 603 | if ($collection) { 604 | $this->collection = $this->connection->getCollection($collection); 605 | } 606 | 607 | return parent::from($collection); 608 | } 609 | 610 | /** 611 | * Run a truncate statement on the table. 612 | */ 613 | public function truncate() 614 | { 615 | $result = $this->collection->drop(); 616 | 617 | return (1 == (int) $result->ok); 618 | } 619 | 620 | /** 621 | * Get an array with the values of a given column. 622 | * 623 | * @param string $column 624 | * @param string $key 625 | * @return array 626 | */ 627 | public function lists($column, $key = null) 628 | { 629 | if ($key == '_id') { 630 | $results = new Collection($this->get([$column, $key])); 631 | 632 | // Convert ObjectID's to strings so that lists can do its work. 633 | $results = $results->map(function ($item) { 634 | $item['_id'] = (string) $item['_id']; 635 | 636 | return $item; 637 | }); 638 | 639 | return $results->lists($column, $key)->all(); 640 | } 641 | 642 | return parent::lists($column, $key); 643 | } 644 | 645 | /** 646 | * Create a raw database expression. 647 | * 648 | * @param closure $expression 649 | * @return mixed 650 | */ 651 | public function raw($expression = null) 652 | { 653 | // Execute the closure on the cassandra collection 654 | if ($expression instanceof Closure) { 655 | return call_user_func($expression, $this->collection); 656 | } // Create an expression for the given value 657 | elseif (! is_null($expression)) { 658 | return new Expression($expression); 659 | } 660 | 661 | // Quick access to the cassandra collection 662 | return $this->collection; 663 | } 664 | 665 | /** 666 | * Append one or more values to an array. 667 | * 668 | * @param mixed $column 669 | * @param mixed $value 670 | * @return int 671 | */ 672 | public function push($column, $value = null, $unique = false) 673 | { 674 | // Use the addToSet operator in case we only want unique items. 675 | $operator = $unique ? '$addToSet' : '$push'; 676 | 677 | // Check if we are pushing multiple values. 678 | $batch = (is_array($value) and array_keys($value) === range(0, count($value) - 1)); 679 | 680 | if (is_array($column)) { 681 | $query = [$operator => $column]; 682 | } elseif ($batch) { 683 | $query = [$operator => [$column => ['$each' => $value]]]; 684 | } else { 685 | $query = [$operator => [$column => $value]]; 686 | } 687 | 688 | return $this->performUpdate($query); 689 | } 690 | 691 | /** 692 | * Remove one or more values from an array. 693 | * 694 | * @param mixed $column 695 | * @param mixed $value 696 | * @return int 697 | */ 698 | public function pull($column, $value = null) 699 | { 700 | // Check if we passed an associative array. 701 | $batch = (is_array($value) and array_keys($value) === range(0, count($value) - 1)); 702 | 703 | // If we are pulling multiple values, we need to use $pullAll. 704 | $operator = $batch ? '$pullAll' : '$pull'; 705 | 706 | if (is_array($column)) { 707 | $query = [$operator => $column]; 708 | } else { 709 | $query = [$operator => [$column => $value]]; 710 | } 711 | 712 | return $this->performUpdate($query); 713 | } 714 | 715 | /** 716 | * Remove one or more fields. 717 | * 718 | * @param mixed $columns 719 | * @return int 720 | */ 721 | public function drop($columns) 722 | { 723 | if (! is_array($columns)) { 724 | $columns = [$columns]; 725 | } 726 | 727 | $fields = []; 728 | 729 | foreach ($columns as $column) { 730 | $fields[$column] = 1; 731 | } 732 | 733 | $query = ['$unset' => $fields]; 734 | 735 | return $this->performUpdate($query); 736 | } 737 | 738 | /** 739 | * Get a new instance of the query builder. 740 | * 741 | * @return Builder 742 | */ 743 | public function newQuery() 744 | { 745 | return new Builder($this->connection, $this->processor); 746 | } 747 | 748 | /** 749 | * Perform an update query. 750 | * 751 | * @param array $query 752 | * @param array $options 753 | * @return int 754 | */ 755 | protected function performUpdate($query, array $options = []) 756 | { 757 | // Update multiple items by default. 758 | if (! array_key_exists('multiple', $options)) { 759 | $options['multiple'] = true; 760 | } 761 | 762 | $wheres = $this->compileWheres(); 763 | $result = $this->collection->UpdateMany($wheres, $query, $options); 764 | if (1 == (int) $result->isAcknowledged()) { 765 | return $result->getModifiedCount() ? $result->getModifiedCount() : $result->getUpsertedCount(); 766 | } 767 | 768 | return 0; 769 | } 770 | 771 | /** 772 | * Convert a key to ObjectID if needed. 773 | * 774 | * @param mixed $id 775 | * @return mixed 776 | */ 777 | public function convertKey($id) 778 | { 779 | if (is_string($id) and strlen($id) === 24 and ctype_xdigit($id)) { 780 | return new ObjectID($id); 781 | } 782 | 783 | return $id; 784 | } 785 | 786 | /** 787 | * Add a basic where clause to the query. 788 | * 789 | * @param string $column 790 | * @param string $operator 791 | * @param mixed $value 792 | * @param string $boolean 793 | * @return \Illuminate\Database\Query\Builder|static 794 | * 795 | * @throws \InvalidArgumentException 796 | */ 797 | public function where($column, $operator = null, $value = null, $boolean = 'and') 798 | { 799 | $params = func_get_args(); 800 | 801 | // Remove the leading $ from operators. 802 | if (func_num_args() == 3) { 803 | $operator = &$params[1]; 804 | 805 | if (starts_with($operator, '$')) { 806 | $operator = substr($operator, 1); 807 | } 808 | } 809 | 810 | return call_user_func_array('parent::where', $params); 811 | } 812 | 813 | /** 814 | * Compile the where array. 815 | * 816 | * @return array 817 | */ 818 | protected function compileWheres() 819 | { 820 | // The wheres to compile. 821 | $wheres = $this->wheres ?: []; 822 | 823 | // We will add all compiled wheres to this array. 824 | $compiled = []; 825 | 826 | foreach ($wheres as $i => &$where) { 827 | // Make sure the operator is in lowercase. 828 | if (isset($where['operator'])) { 829 | $where['operator'] = strtolower($where['operator']); 830 | 831 | // Operator conversions 832 | $convert = [ 833 | 'regexp' => 'regex', 834 | 'elemmatch' => 'elemMatch', 835 | 'geointersects' => 'geoIntersects', 836 | 'geowithin' => 'geoWithin', 837 | 'nearsphere' => 'nearSphere', 838 | 'maxdistance' => 'maxDistance', 839 | 'centersphere' => 'centerSphere', 840 | 'uniquedocs' => 'uniqueDocs', 841 | ]; 842 | 843 | if (array_key_exists($where['operator'], $convert)) { 844 | $where['operator'] = $convert[$where['operator']]; 845 | } 846 | } 847 | 848 | // Convert id's. 849 | if (isset($where['column']) and ($where['column'] == '_id' or ends_with($where['column'], '._id'))) { 850 | // Multiple values. 851 | if (isset($where['values'])) { 852 | foreach ($where['values'] as &$value) { 853 | $value = $this->convertKey($value); 854 | } 855 | } // Single value. 856 | elseif (isset($where['value'])) { 857 | $where['value'] = $this->convertKey($where['value']); 858 | } 859 | } 860 | 861 | // Convert DateTime values to UTCDateTime. 862 | if (isset($where['value']) and $where['value'] instanceof DateTime) { 863 | $where['value'] = new UTCDateTime($where['value']->getTimestamp() * 1000); 864 | } 865 | 866 | // The next item in a "chain" of wheres devices the boolean of the 867 | // first item. So if we see that there are multiple wheres, we will 868 | // use the operator of the next where. 869 | if ($i == 0 and count($wheres) > 1 and $where['boolean'] == 'and') { 870 | $where['boolean'] = $wheres[$i + 1]['boolean']; 871 | } 872 | 873 | // We use different methods to compile different wheres. 874 | $method = "compileWhere{$where['type']}"; 875 | $result = $this->{$method}($where); 876 | 877 | // Wrap the where with an $or operator. 878 | if ($where['boolean'] == 'or') { 879 | $result = ['$or' => [$result]]; 880 | } // If there are multiple wheres, we will wrap it with $and. This is needed 881 | // to make nested wheres work. 882 | elseif (count($wheres) > 1) { 883 | $result = ['$and' => [$result]]; 884 | } 885 | 886 | // Merge the compiled where with the others. 887 | $compiled = array_merge_recursive($compiled, $result); 888 | } 889 | 890 | return $compiled; 891 | } 892 | 893 | protected function compileWhereBasic($where) 894 | { 895 | extract($where); 896 | 897 | // Replace like with a Regex instance. 898 | if ($operator == 'like') { 899 | $operator = '='; 900 | 901 | // Convert to regular expression. 902 | $regex = preg_replace('#(^|[^\\\])%#', '$1.*', preg_quote($value)); 903 | 904 | // Convert like to regular expression. 905 | if (! starts_with($value, '%')) { 906 | $regex = '^' . $regex; 907 | } 908 | if (! ends_with($value, '%')) { 909 | $regex = $regex . '$'; 910 | } 911 | 912 | $value = new Regex($regex, 'i'); 913 | } // Manipulate regexp operations. 914 | elseif (in_array($operator, ['regexp', 'not regexp', 'regex', 'not regex'])) { 915 | // Automatically convert regular expression strings to Regex objects. 916 | if (! $value instanceof Regex) { 917 | $e = explode('/', $value); 918 | $flag = end($e); 919 | $regstr = substr($value, 1, -(strlen($flag) + 1)); 920 | $value = new Regex($regstr, $flag); 921 | } 922 | 923 | // For inverse regexp operations, we can just use the $not operator 924 | // and pass it a Regex instence. 925 | if (starts_with($operator, 'not')) { 926 | $operator = 'not'; 927 | } 928 | } 929 | 930 | if (! isset($operator) or $operator == '=') { 931 | $query = [$column => $value]; 932 | } elseif (array_key_exists($operator, $this->conversion)) { 933 | $query = [$column => [$this->conversion[$operator] => $value]]; 934 | } else { 935 | $query = [$column => ['$' . $operator => $value]]; 936 | } 937 | 938 | return $query; 939 | } 940 | 941 | protected function compileWhereNested($where) 942 | { 943 | extract($where); 944 | 945 | return $query->compileWheres(); 946 | } 947 | 948 | protected function compileWhereIn($where) 949 | { 950 | extract($where); 951 | 952 | return [$column => ['$in' => array_values($values)]]; 953 | } 954 | 955 | protected function compileWhereNotIn($where) 956 | { 957 | extract($where); 958 | 959 | return [$column => ['$nin' => array_values($values)]]; 960 | } 961 | 962 | protected function compileWhereNull($where) 963 | { 964 | $where['operator'] = '='; 965 | $where['value'] = null; 966 | 967 | return $this->compileWhereBasic($where); 968 | } 969 | 970 | protected function compileWhereNotNull($where) 971 | { 972 | $where['operator'] = '!='; 973 | $where['value'] = null; 974 | 975 | return $this->compileWhereBasic($where); 976 | } 977 | 978 | protected function compileWhereBetween($where) 979 | { 980 | extract($where); 981 | 982 | if ($not) { 983 | return [ 984 | '$or' => [ 985 | [ 986 | $column => [ 987 | '$lte' => $values[0], 988 | ], 989 | ], 990 | [ 991 | $column => [ 992 | '$gte' => $values[1], 993 | ], 994 | ], 995 | ], 996 | ]; 997 | } else { 998 | return [ 999 | $column => [ 1000 | '$gte' => $values[0], 1001 | '$lte' => $values[1], 1002 | ], 1003 | ]; 1004 | } 1005 | } 1006 | 1007 | protected function compileWhereRaw($where) 1008 | { 1009 | return $where['sql']; 1010 | } 1011 | 1012 | /** 1013 | * Handle dynamic method calls into the method. 1014 | * 1015 | * @param string $method 1016 | * @param array $parameters 1017 | * @return mixed 1018 | */ 1019 | public function __call($method, $parameters) 1020 | { 1021 | if ($method == 'unset') { 1022 | return call_user_func_array([$this, 'drop'], $parameters); 1023 | } 1024 | 1025 | return parent::__call($method, $parameters); 1026 | } 1027 | } 1028 | --------------------------------------------------------------------------------