├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── composer.lock ├── docker-compose.yml ├── phpunit.xml ├── src ├── Auth │ ├── DatabaseTokenRepository.php │ ├── PasswordBrokerManager.php │ ├── PasswordResetServiceProvider.php │ └── User.php ├── Collection.php ├── Connection.php ├── CouchDBQueueServiceProvider.php ├── Eloquent │ ├── Builder.php │ ├── EmbedsRelations.php │ ├── HybridRelations.php │ ├── Model.php │ └── SoftDeletes.php ├── Exceptions │ ├── QueryException.php │ └── UniqueConstraintException.php ├── Helpers │ └── Arr.php ├── Passport │ ├── AuthCode.php │ ├── Bridge │ │ └── RefreshTokenRepository.php │ ├── Client.php │ ├── PassportServiceProvider.php │ ├── PersonalAccessClient.php │ └── Token.php ├── Query │ ├── Builder.php │ ├── Grammar.php │ └── Processor.php ├── Queue │ ├── CouchConnector.php │ ├── CouchJob.php │ ├── CouchQueue.php │ └── Failed │ │ └── CouchFailedJobProvider.php ├── Relations │ ├── BelongsTo.php │ ├── BelongsToMany.php │ ├── EmbedsMany.php │ ├── EmbedsOne.php │ ├── EmbedsOneOrMany.php │ ├── HasMany.php │ ├── HasOne.php │ ├── MorphTo.php │ └── MorphToMany.php ├── Schema │ └── Grammar.php └── ServiceProvider.php └── tests ├── AuthTest.php ├── CollectionTest.php ├── ConnectionTest.php ├── EmbeddedRelationsTest.php ├── ExampleTest.php ├── ModelTest.php ├── QueryBuilderTest.php ├── QueryTest.php ├── QueueTest.php ├── RelationsTest.php ├── SeederTest.php ├── TestCase.php ├── ValidationTest.php ├── config ├── database.php └── queue.php ├── models ├── Address.php ├── Book.php ├── Client.php ├── Company.php ├── Country.php ├── Group.php ├── Item.php ├── Movie.php ├── Photo.php ├── Role.php ├── Soft.php ├── Tag.php └── User.php └── seeds ├── DatabaseSeeder.php └── UserTableSeeder.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Laravel 5 # 2 | vendor/ 3 | .php_cs.cache 4 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: bionic 3 | sudo: required 4 | services: 5 | - docker 6 | php: 7 | - 7.2 8 | - 7.3 9 | 10 | before_script: 11 | - docker run -d -p 5984:5984 couchdb:2.3.1 12 | - sleep 10 13 | - curl -X PUT localhost:5984/test 14 | - composer self-update 15 | - composer install --prefer-source --no-interaction --dev 16 | 17 | script: vendor/bin/phpunit --coverage-clover=coverage.xml 18 | 19 | after_success: 20 | - bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel CouchDB 2 | 3 | 4 | [![Build Status](http://img.shields.io/travis/robsonvn/laravel-couchdb.svg?branch=master)](https://travis-ci.org/robsonvn/laravel-couchdb) [![StyleCI](https://styleci.io/repos/90929490/shield?style=flat)](https://styleci.io/repos/90929490) 5 | [![codecov.io](https://codecov.io/gh/robsonvn/laravel-couchdb/coverage.svg?branch=master)](https://codecov.io/gh/robsonvn/laravel-couchdb) 6 | 7 | Laravel CouchDB is an Eloquent model and Query builder with support for **CouchDB 2.x**, using the original Laravel API. This library extends the original Laravel classes, so it uses exactly the same methods. 8 | 9 | ## Good to know before using it 10 | 11 | * CouchDB has many limitations dealing with Mango Query that force us to process somethings in memory, which directly impacts on our library performance, please check out the [Couch Limitations](#couchdb-limitations) and the [Limitations](#limitations) sections for more details. 12 | 13 | Table of contents 14 | ----------------- 15 | * [Installation](#installation) 16 | * [Configuration](#configuration) 17 | * [Eloquent](#eloquent) 18 | * [Query Builder](#query-builder) 19 | * [Extensions](#extensions) 20 | * [Examples](#examples) 21 | * [Inserts, updates and deletes](#inserts-updates-and-deletes) 22 | * [Relations](#relations) 23 | * [CouchDB specific operators](#couchdb-specific-operators) 24 | * [CouchDB Limitations](#couchdb-limitations) 25 | * [Limitations](#limitations) 26 | * [TODO](#todo) 27 | 28 | ## Installation 29 | 30 | 31 | Installation using composer: 32 | 33 | ``` 34 | composer require robsonvn/laravel-couchdb 35 | ``` 36 | 37 | And add the service provider in `config/app.php`: 38 | 39 | ```php 40 | Robsonvn\CouchDB\ServiceProvider::class, 41 | ``` 42 | ### Laravel version Compatibility 43 | For now, this project only works with Laravel 5.4.x 44 | 45 | 46 | Configuration 47 | ------------- 48 | 49 | Change your default database connection name in `config/database.php`: 50 | 51 | ```php 52 | 'default' => env('DB_CONNECTION', 'couchdb'), 53 | ``` 54 | 55 | And add a new couchdb connection: 56 | 57 | ```php 58 | 'couchdb' => [ 59 | 'driver' => 'couchdb', 60 | 'type' => env('DB_CONNECTION_TYPE', 'socket'), 61 | 'host' => env('DB_HOST', 'localhost'), 62 | 'ip' => env('DB_IP', null), 63 | 'port' => env('DB_PORT', '5984'), 64 | 'dbname' => env('DB_DATABASE', 'forge'), 65 | 'user' => env('DB_USERNAME', null), 66 | 'password' => env('DB_PASSWORD', null), 67 | 'logging' => env('DB_LOGGING', false), 68 | ], 69 | ``` 70 | 71 | And this on yours .env file 72 | 73 | ``` 74 | DB_CONNECTION=couchdb 75 | DB_HOST=dbhost 76 | DB_PORT=5984 77 | DB_DATABASE=dbname 78 | DB_USERNAME= 79 | DB_PASSWORD= 80 | ``` 81 | ***Please note, the database user must be an admin since this library creates indexes on the fly (design_docs)*** 82 | 83 | You can read more about CouchDB Authorization [here](http://docs.couchdb.org/en/2.1.1/intro/security.html#authorization). 84 | 85 | Eloquent 86 | -------- 87 | 88 | This package includes a CouchDB enabled Eloquent class that you can use to define models for corresponding collections. 89 | 90 | ```php 91 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 92 | 93 | class Book extends Eloquent{} 94 | ``` 95 | 96 | Note that we did not tell Eloquent which collection to use for the `Book` model. Just like the original Eloquent, the lower-case, plural name of the class will be used as the collection name unless another name is explicitly specified. You may specify a custom collection (alias for table) by defining a `collection` property on your model: 97 | 98 | ```php 99 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 100 | 101 | class Book extends Eloquent{ 102 | protected $collection = 'books_collection'; 103 | } 104 | ``` 105 | 106 | 107 | Query Builder 108 | ------------- 109 | 110 | The database driver plugs right into the original query builder. When using couchdb 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 couchdb specific operators/operations. 111 | 112 | ```php 113 | $users = DB::collection('users')->get(); 114 | 115 | $user = DB::collection('users')->where('name', 'John')->first(); 116 | ``` 117 | 118 | If you did not change your default database connection, you will need to specify it when querying. 119 | 120 | ```php 121 | $user = DB::connection('couchdb')->collection('users')->get(); 122 | ``` 123 | 124 | Read more about the query builder on http://laravel.com/docs/queries 125 | 126 | Extensions 127 | ---------- 128 | 129 | ### Auth 130 | 131 | If you want to use Laravel's native Auth functionality, register this included service provider: 132 | 133 | ```php 134 | Robsonvn\CouchDB\Auth\PasswordResetServiceProvider::class, 135 | ``` 136 | 137 | This service provider will slightly modify the internal DatabaseTokenRepository to add support for CouchDB 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. 138 | 139 | You also needs to extends the CouchDB Authenticatable class in your User class instead of the native one. 140 | 141 | ```php 142 | use Robsonvn\CouchDB\Auth\User as Authenticatable; 143 | 144 | class User extends Authenticatable 145 | { 146 | } 147 | ``` 148 | 149 | ### Queues 150 | 151 | If you want to use CouchDB as your database backend, change the the driver in `config/queue.php`: 152 | 153 | ```php 154 | 'connections' => [ 155 | 'database' => [ 156 | 'driver' => 'couchdb', 157 | 'table' => 'jobs', 158 | 'queue' => 'default', 159 | 'expire' => 60, 160 | ], 161 | ``` 162 | 163 | If you want to use CouchDB to handle failed jobs, change the database in `config/queue.php`: 164 | 165 | ```php 166 | 'failed' => [ 167 | 'database' => 'couchdb', 168 | 'table' => 'failed_jobs', 169 | ], 170 | ``` 171 | 172 | And add the service provider in `config/app.php`: 173 | 174 | ```php 175 | Robsonvn\CouchDB\CouchDBQueueServiceProvider::class, 176 | ``` 177 | Examples 178 | -------- 179 | 180 | ### Basic Usage 181 | 182 | **Retrieving All Models** 183 | 184 | ```php 185 | $users = User::all(); 186 | ``` 187 | 188 | **Retrieving A Record By Primary Key** 189 | 190 | ```php 191 | $user = User::find('517c43667db388101e00000f'); 192 | ``` 193 | 194 | **Wheres** 195 | 196 | ```php 197 | $users = User::where('votes', '>', 100)->take(10)->get(); 198 | ``` 199 | 200 | **Or Statements** 201 | 202 | ```php 203 | $users = User::where('votes', '>', 100)->orWhere('name', 'John')->get(); 204 | ``` 205 | 206 | **And Statements** 207 | 208 | ```php 209 | $users = User::where('votes', '>', 100)->where('name', '=', 'John')->get(); 210 | ``` 211 | 212 | **Using Where In With An Array** 213 | 214 | ```php 215 | $users = User::whereIn('age', [16, 18, 20])->get(); 216 | ``` 217 | 218 | When using `whereNotIn` objects will be returned if the field is non existent. Combine with `whereNotNull('age')` to leave out those documents. 219 | 220 | **Using Where Between** 221 | 222 | ```php 223 | $users = User::whereBetween('votes', [1, 100])->get(); 224 | ``` 225 | 226 | **Where null** 227 | 228 | ```php 229 | $users = User::whereNull('updated_at')->get(); 230 | ``` 231 | 232 | **Order By** 233 | 234 | ```php 235 | $users = User::orderBy('name', 'desc')->get(); 236 | ``` 237 | 238 | **Offset & Limit** 239 | 240 | ```php 241 | $users = User::skip(10)->take(5)->get(); 242 | ``` 243 | 244 | **Advanced Wheres** 245 | 246 | ```php 247 | $users = User::where('name', '=', 'John')->orWhere(function($query) 248 | { 249 | $query->where('votes', '>', 100) 250 | ->where('title', '<>', 'Admin'); 251 | }) 252 | ->get(); 253 | ``` 254 | 255 | **Like (case-sensitive)** 256 | 257 | ```php 258 | $user = Comment::where('body', 'like', '%spam%')->get(); 259 | ``` 260 | **Like (case-insensitive)** 261 | 262 | ```php 263 | $user = Comment::where('body', 'ilike', '%spam%')->get(); 264 | ``` 265 | 266 | **Incrementing or decrementing a value of a column** 267 | 268 | Perform increments or decrements (default 1) on specified attributes: 269 | 270 | ```php 271 | User::where('name', 'John Doe')->increment('age'); 272 | User::where('name', 'Jaques')->decrement('weight', 50); 273 | ``` 274 | 275 | The number of updated objects is returned: 276 | 277 | ```php 278 | $count = User->increment('age'); 279 | ``` 280 | 281 | You may also specify additional columns to update: 282 | 283 | ```php 284 | User::where('age', '29')->increment('age', 1, ['group' => 'thirty something']); 285 | User::where('bmi', 30)->decrement('bmi', 1, ['category' => 'overweight']); 286 | ``` 287 | 288 | **Soft deleting** 289 | 290 | 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: 291 | 292 | ```php 293 | use Robsonvn\CouchDB\Eloquent\SoftDeletes; 294 | 295 | class User extends Eloquent { 296 | 297 | use SoftDeletes; 298 | 299 | protected $dates = ['deleted_at']; 300 | 301 | } 302 | ``` 303 | 304 | For more information check http://laravel.com/docs/eloquent#soft-deleting 305 | 306 | ### CouchDB specific operators 307 | 308 | **Exists** 309 | 310 | Matches documents that have the specified field. 311 | 312 | ```php 313 | User::where('age', 'exists', true)->get(); 314 | ``` 315 | 316 | **All** 317 | 318 | Matches arrays that contain all elements specified in the query. 319 | 320 | ```php 321 | User::where('roles', 'all', ['moderator', 'author'])->get(); 322 | ``` 323 | 324 | **Size** 325 | 326 | Selects documents if the array field is a specified size. 327 | 328 | ```php 329 | User::where('tags', 'size', 3)->get(); 330 | ``` 331 | 332 | **Regex** 333 | 334 | Selects documents where values match a specified regular expression. 335 | 336 | ```php 337 | User::where('name', 'regex', '(?i).*doe$')->get(); 338 | User::where('name', 'not regex', '(?i).*doe$')->get() 339 | ``` 340 | 341 | **NOTE:** Mango query uses Erlang regular expression implementation. 342 | 343 | >Most selector expressions work exactly as you would expect for the given operator. The matching algorithms used by the $regex operator are currently based on the Perl Compatible Regular Expression (PCRE) library. However, not all of the PCRE library is implemented, and some parts of the $regex operator go beyond what PCRE offers. For more information about what is implemented, see the Erlang Regular Expression information http://erlang.org/doc/man/re.html. 344 | 345 | 346 | **Type** 347 | 348 | Selects documents if a field is of the specified type. 349 | Valid values are "null", "boolean", "number", "string", "array", and "object". 350 | 351 | ```php 352 | User::where('age', 'type', 2)->get(); 353 | ``` 354 | 355 | **Mod** 356 | 357 | Performs a modulo operation on the value of a field and selects documents with a specified result. 358 | 359 | ```php 360 | User::where('age', 'mod', [10, 0])->get(); 361 | ``` 362 | ### Inserts, updates and deletes 363 | 364 | Inserting, updating and deleting records works just like the original Eloquent. 365 | 366 | **Saving a new model** 367 | 368 | ```php 369 | $user = new User; 370 | $user->name = 'John'; 371 | $user->save(); 372 | ``` 373 | 374 | You may also use the create method to save a new model in a single line: 375 | 376 | ```php 377 | User::create(['name' => 'John']); 378 | ``` 379 | 380 | **Updating a model** 381 | 382 | To update a model, you may retrieve it, change an attribute, and use the save method. 383 | 384 | ```php 385 | $user = User::first(); 386 | $user->email = 'john@foo.com'; 387 | $user->save(); 388 | ``` 389 | 390 | **Deleting a model** 391 | 392 | To delete a model, simply call the delete method on the instance: 393 | 394 | ```php 395 | $user = User::first(); 396 | $user->delete(); 397 | ``` 398 | 399 | Or deleting a model by its key: 400 | 401 | ```php 402 | User::destroy('517c43667db388101e00000f'); 403 | ``` 404 | 405 | For more information about model manipulation, check http://laravel.com/docs/eloquent#insert-update-delete 406 | 407 | ### Dates 408 | 409 | Eloquent allows you to work with Carbon/DateTime object. Internally, these dates will be converted to a formated string 'yyyy-mm-dd H:i:s' 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 410 | 411 | Example: 412 | 413 | ```php 414 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 415 | 416 | class User extends Eloquent { 417 | 418 | protected $dates = ['birthday']; 419 | 420 | } 421 | ``` 422 | 423 | Which allows you to execute queries like: 424 | 425 | ```php 426 | $users = User::where('birthday', '>', new DateTime('-18 years'))->get(); 427 | ``` 428 | 429 | ### Relations 430 | 431 | Supported relations are: 432 | 433 | - hasOne 434 | - hasMany 435 | - belongsTo 436 | - belongsToMany 437 | - morphToMany 438 | - embedsOne 439 | - embedsMany 440 | 441 | Example: 442 | 443 | ```php 444 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 445 | 446 | class User extends Eloquent { 447 | 448 | public function items() 449 | { 450 | return $this->hasMany('Item'); 451 | } 452 | 453 | } 454 | ``` 455 | 456 | And the inverse relation: 457 | 458 | ```php 459 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 460 | 461 | class Item extends Eloquent { 462 | 463 | public function user() 464 | { 465 | return $this->belongsTo('User'); 466 | } 467 | 468 | } 469 | ``` 470 | 471 | 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`: 472 | 473 | ```php 474 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 475 | 476 | class User extends Eloquent { 477 | 478 | public function groups() 479 | { 480 | return $this->belongsToMany('Group', null, 'user_ids', 'group_ids'); 481 | } 482 | 483 | } 484 | ``` 485 | 486 | 487 | 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 488 | 489 | ### EmbedsMany Relations 490 | 491 | 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. 492 | 493 | **REMEMBER**: these relations return Eloquent collections, they don't return query builder objects! 494 | 495 | ```php 496 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 497 | 498 | class User extends Eloquent { 499 | 500 | public function books() 501 | { 502 | return $this->embedsMany('Book'); 503 | } 504 | 505 | } 506 | ``` 507 | 508 | You access the embedded models through the dynamic property: 509 | 510 | ```php 511 | $books = User::first()->books; 512 | ``` 513 | 514 | The inverse relation is auto*magically* available, you don't need to define this reverse relation. 515 | 516 | ```php 517 | $user = $book->user; 518 | ``` 519 | 520 | Inserting and updating embedded models works similar to the `hasMany` relation: 521 | 522 | ```php 523 | $book = new Book(['title' => 'A Game of Thrones']); 524 | 525 | $user = User::first(); 526 | 527 | $book = $user->books()->save($book); 528 | // or 529 | $book = $user->books()->create(['title' => 'A Game of Thrones']) 530 | ``` 531 | 532 | You can update embedded models using their `save` method: 533 | 534 | ```php 535 | $book = $user->books()->first(); 536 | 537 | $book->title = 'A Game of Thrones'; 538 | 539 | $book->save(); 540 | ``` 541 | 542 | You can remove an embedded model by using the `destroy` method on the relation, or the `delete` method on the model: 543 | 544 | ```php 545 | $book = $user->books()->first(); 546 | 547 | $book->delete(); 548 | // or 549 | $user->books()->destroy($book); 550 | ``` 551 | 552 | 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: 553 | 554 | ```php 555 | $user->books()->associate($book); 556 | 557 | $user->save(); 558 | ``` 559 | 560 | 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: 561 | 562 | ```php 563 | return $this->embedsMany('Book', 'local_key'); 564 | ``` 565 | 566 | 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 567 | 568 | ### EmbedsOne Relations 569 | 570 | The embedsOne relation is similar to the EmbedsMany relation, but only embeds a single model. 571 | 572 | ```php 573 | use Robsonvn\CouchDB\Eloquent\Model as Eloquent; 574 | 575 | class Book extends Eloquent { 576 | 577 | public function author() 578 | { 579 | return $this->embedsOne('Author'); 580 | } 581 | 582 | } 583 | ``` 584 | 585 | You access the embedded models through the dynamic property: 586 | 587 | ```php 588 | $author = Book::first()->author; 589 | ``` 590 | 591 | Inserting and updating embedded models works similar to the `hasOne` relation: 592 | 593 | ```php 594 | $author = new Author(['name' => 'John Doe']); 595 | 596 | $book = Books::first(); 597 | 598 | $author = $book->author()->save($author); 599 | // or 600 | $author = $book->author()->create(['name' => 'John Doe']); 601 | ``` 602 | 603 | You can update the embedded model using the `save` method: 604 | 605 | ```php 606 | $author = $book->author; 607 | 608 | $author->name = 'Jane Doe'; 609 | $author->save(); 610 | ``` 611 | 612 | You can replace the embedded model with a new model like this: 613 | 614 | ```php 615 | $newAuthor = new Author(['name' => 'Jane Doe']); 616 | $book->author()->save($newAuthor); 617 | ``` 618 | 619 | ### Raw Expressions 620 | 621 | These expressions will be injected directly into the query. 622 | 623 | ```php 624 | User::whereRaw(['age' => array('$gt' => 30, '$lt' => 40)])->get(); 625 | ``` 626 | 627 | You can also perform raw expressions on the internal CouchDBCollection 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. 628 | 629 | ```php 630 | // Returns a collection of User models. 631 | $models = User::raw(function($collection) 632 | { 633 | return $collection->find(['_id'=>['$gt'=>null]]); 634 | }); 635 | 636 | // Returns the original CouchDB response. 637 | $cursor = DB::collection('users')->raw(function($collection) 638 | { 639 | return $collection->find(['_id'=>['$gt'=>null]]); 640 | }); 641 | ``` 642 | 643 | The internal CouchDBClient can be accessed like this: 644 | 645 | ```php 646 | $client = DB::getCouchDBClient(); 647 | ``` 648 | 649 | CouchDB Limitations 650 | ------------ 651 | * Currently, there's no way to update and delete using Mango Query. In this case, we have to query the data, bring it to memory, update the fields and bulk an update. 652 | * CouchDB is really touchy in matter of indexes, even the documentation [recommends](http://docs.couchdb.org/en/2.0.0/api/database/find.html#index-selection) to always explicit the index that your query should use. In this case, **we are automatically creating all necessaries index on the fly**. 653 | * CouchDB does not have the concept of collection as MongoDB, so we are using "collections" by adding an attribute (type) in every single document. Please, treat type as a reserved attribute. Use of collections is not optional. 654 | 655 | Limitations 656 | ------------ 657 | 658 | * Due the way we're creating index this library does not work with the Full Text Search engine enabled yet. 659 | * Aggregation, group by and distinct operations is not supported yet. 660 | * If you want to use any library that extends the original Eloquent classes you may have to fork it and change to our classes. 661 | 662 | 663 | TODO 664 | ------------ 665 | 666 | * Add compatibility to work with Full Text Search engine. 667 | * Add support to aggregation, group by and distinct operations. 668 | * Create a query cursor. 669 | * Add support to get casted attribute using doting notation. 670 | 671 | ## Special Thanks 672 | 673 | [Fred Booking](https://www.fredbooking.com) for supporting this project. 674 | 675 | [Jens Segers](https://github.com/jenssegers) and the [Laravel MongoDB contributors](https://github.com/jenssegers/laravel-mongodb/graphs/contributors) because many of the code and structure of this project came from there. 676 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robsonvn/laravel-couchdb", 3 | "description": "A CouchDB v2.0.0 based Eloquent model and Query builder for Laravel", 4 | "authors": [ 5 | { 6 | "name": "Robson Nascimento", 7 | "email": "robsonvnasc@gmail.com" 8 | } 9 | ], 10 | "license" : "MIT", 11 | "require": { 12 | "php": "^7.2", 13 | "illuminate/database": "5.4.*", 14 | "illuminate/support": "5.4.*", 15 | "illuminate/container": "5.4.*", 16 | "illuminate/events": "5.4.*", 17 | "doctrine/couchdb": "^2.0.0-alpha1", 18 | "adbario/php-dot-notation": "^1.2" 19 | }, 20 | "require-dev":{ 21 | "laravel/passport": "^3.0.0", 22 | "phpunit/phpunit": "~6.0", 23 | "orchestra/testbench": "^3.1", 24 | "mockery/mockery": "^0.9" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Robsonvn\\CouchDB\\": "src" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "classmap": [ 33 | "tests/TestCase.php", 34 | "tests/models", 35 | "tests/seeds" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | 4 | couchdb: 5 | image: couchdb:2.3.1 6 | volumes: 7 | - couchdb-data:/opt/couchdb/data 8 | ports: 9 | - "5984:5984" 10 | - "6984:6984" 11 | restart: always 12 | 13 | php: 14 | image: php:7.3.17-cli 15 | working_dir: /app 16 | volumes: 17 | - ./:/app 18 | depends_on: 19 | - couchdb 20 | environment: 21 | - COUCHDB_HOST=couchdb 22 | - COUCHDB_DB_NAME=test 23 | volumes: 24 | couchdb-data: -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | ./tests/TestCase.php 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Auth/DatabaseTokenRepository.php: -------------------------------------------------------------------------------- 1 | $email, 'token' => $this->hasher->make($token), 'created_at' => (new Carbon())->format('Y-m-d H:i:s')]; 15 | } 16 | 17 | /** 18 | * @inheritdoc 19 | */ 20 | protected function tokenExpired($createdAt) 21 | { 22 | return Carbon::createFromFormat('Y-m-d H:i:s',$createdAt)->addSeconds($this->expires)->isPast(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Auth/PasswordBrokerManager.php: -------------------------------------------------------------------------------- 1 | app['db']->connection(), 15 | $this->app['hash'], 16 | $config['table'], 17 | $this->app['config']['app.key'], 18 | $config['expire'] 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Auth/PasswordResetServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('auth.password.tokens', function ($app) { 16 | $connection = $app['db']->connection(); 17 | 18 | // The database token repository is an implementation of the token repository 19 | // interface, and is responsible for the actual storing of auth tokens and 20 | // their e-mail addresses. We will inject this table and hash key to it. 21 | $table = $app['config']['auth.password.table']; 22 | 23 | $key = $app['config']['app.key']; 24 | 25 | $expire = $app['config']->get('auth.password.expire', 60); 26 | 27 | return new DatabaseTokenRepository($connection, $table, $key, $expire); 28 | }); 29 | }*/ 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | protected function registerPasswordBroker() 35 | { 36 | $this->app->singleton('auth.password', function ($app) { 37 | return new PasswordBrokerManager($app); 38 | }); 39 | 40 | $this->app->bind('auth.password.broker', function ($app) { 41 | return $app->make('auth.password')->broker(); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Auth/User.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 22 | $this->collection = $collection; 23 | } 24 | 25 | public function __call($method, $parameters) 26 | { 27 | $parameters[0]['type'] = $this->collection; 28 | $result = call_user_func_array([$this->connection->getCouchDBClient(), $method], $parameters); 29 | 30 | return $result; 31 | } 32 | 33 | public function findDocuments($ids, $limit = null, $offset = null) 34 | { 35 | $client = $this->connection->getCouchDBClient(); 36 | 37 | return $client->findDocuments($ids, $limit, $offset); 38 | } 39 | 40 | public function find(MangoQuery $query, $options = null) 41 | { 42 | $selector = $query->selector(); 43 | $query->selector($selector); 44 | $client = $this->connection->getCouchDBClient(); 45 | return $client->find($query, $options); 46 | } 47 | 48 | public function getName() 49 | { 50 | return $this->collection; 51 | } 52 | 53 | public function deleteMany($documents) 54 | { 55 | $deleted = 0; 56 | $client = $this->connection->getCouchDBClient(); 57 | $bulkUpdater = $client->createBulkUpdater(); 58 | 59 | foreach ($documents as $doc) { 60 | $doc['_deleted'] = true; 61 | $bulkUpdater->updateDocument($doc, $doc['_id']); 62 | } 63 | 64 | $result = $bulkUpdater->execute(); 65 | 66 | if ($result->status == 201) { 67 | $deleted = count($result->body); 68 | } 69 | 70 | return $deleted; 71 | } 72 | 73 | public function insertMany($values) 74 | { 75 | //Force type 76 | foreach ($values as &$value) { 77 | $value['type'] = $this->collection; 78 | } 79 | 80 | $client = $this->connection->getCouchDBClient(); 81 | $bulkUpdater = $client->createBulkUpdater(); 82 | $bulkUpdater->updateDocuments($values); 83 | $response = $bulkUpdater->execute(); 84 | 85 | return $response->body; 86 | } 87 | 88 | public function insertOne($values, $id = null) 89 | { 90 | if ($id) { 91 | $response = $this->putDocument($values, $id); 92 | } else { 93 | $response = $this->postDocument($values); 94 | } 95 | 96 | return $response; 97 | } 98 | 99 | public function findOne($where) 100 | { 101 | $query = new MangoQuery($where); 102 | $response = $this->find($query->limit(1)); 103 | 104 | if ($response->status != 200) { 105 | return; 106 | } 107 | 108 | if (count($response->body['docs']) > 0) { 109 | return $response->body['docs'][0]; 110 | } 111 | } 112 | 113 | public function updateMany(array $documents, $values, array $options = []) 114 | { 115 | foreach ($documents as &$document) { 116 | //update new values 117 | $document = array_merge($document, $values); 118 | $document = $this->applyUpdateOptions($document, $options); 119 | } 120 | 121 | $client = $this->connection->getCouchDBClient(); 122 | $bulkUpdater = $client->createBulkUpdater(); 123 | $bulkUpdater->updateDocuments($documents); 124 | $response = $bulkUpdater->execute(); 125 | 126 | if ($response->status == 201) { 127 | return $response->body; 128 | } 129 | } 130 | 131 | protected function applyUpdateOptions($document, $options) 132 | { 133 | foreach ($options as $option => $value) { 134 | $option = ucfirst(str_replace('$', '', $option)); 135 | $method = 'applyUpdateOption'.$option; 136 | if (method_exists($this, $method)) { 137 | $document = call_user_func_array([$this, $method], [$document, $value]); 138 | } 139 | } 140 | 141 | return $document; 142 | } 143 | 144 | protected function applyUpdateOptionInc($document, $options) 145 | { 146 | $data = new \Adbar\Dot(); 147 | $data->setReference($document); 148 | 149 | foreach ($options as $key => $value) { 150 | $current_value = ($data->get($key)) ?: 0; 151 | $data->set($key, $current_value + $value); 152 | } 153 | 154 | return $document; 155 | } 156 | 157 | protected function applyUpdateOptionUnset($document, $options) 158 | { 159 | return array_diff_key($document, $options); 160 | } 161 | 162 | protected function applyUpdateOptionAddToSet($document, $options) 163 | { 164 | return $this->applyUpdateOptionPush($document, $options, true); 165 | } 166 | 167 | protected function applyUpdateOptionPush($document, $options, $unique = false) 168 | { 169 | foreach ($options as $key => $value) { 170 | if (is_array($value) && array_key_exists('$each', $value)) { 171 | $value = $value['$each']; 172 | } else { 173 | $value = [$value]; 174 | } 175 | 176 | //If there's no value yet 177 | if (!array_key_exists($key, $document)) { 178 | $value = (array) $value; 179 | //apply unique treatment 180 | if ($unique) { 181 | $is_sequencial = (is_array($value) and array_keys($value) === range(0, count($value) - 1)); 182 | $value = array_unique($value); 183 | //if is a sequencial array, reset array index 184 | if ($is_sequencial) { 185 | $value = array_values($value); 186 | } 187 | } 188 | $document[$key] = $value; 189 | continue; 190 | } 191 | 192 | foreach ($value as $v) { 193 | if ($unique && $this->checkIfExists($v, $document[$key])) { 194 | //Throw a unique exception in case of is push a new document with an existing _id 195 | if (is_array($v) && array_key_exists('_id', $v)) { 196 | throw new UniqueConstraintException; 197 | } 198 | } else { 199 | array_push($document[$key], $v); 200 | } 201 | } 202 | } 203 | 204 | return $document; 205 | } 206 | 207 | protected function checkIfExists($new, $documents) 208 | { 209 | if (is_array($new) && array_key_exists('_id', $new)) { 210 | foreach ($documents as $document) { 211 | if (isset($document['_id']) && $document['_id'] == $new['_id']) { 212 | return true; 213 | } 214 | } 215 | } 216 | 217 | return in_array($new, $documents); 218 | } 219 | 220 | protected function applyUpdateOptionPullAll($document, $options) 221 | { 222 | //cast array values into a sequencial array 223 | array_walk($options, function (&$value) { 224 | $is_sequencial = (is_array($value) and array_keys($value) === range(0, count($value) - 1)); 225 | if (!$is_sequencial) { 226 | $value = [$value]; 227 | } 228 | }); 229 | 230 | return Arr::array_diff_recursive($document, $options); 231 | } 232 | 233 | public function createMangoIndex($fields, $index_name) 234 | { 235 | $response = $this->connection->getCouchDBClient()->createMangoIndex($fields, 'mango-indexes', $index_name); 236 | 237 | return in_array($response->status, [200, 201]); 238 | } 239 | 240 | public function __toString() 241 | { 242 | return $this->collection; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | config = $config; 25 | $this->db = MangoClient::create($config); 26 | $this->useDefaultPostProcessor(); 27 | $this->useDefaultQueryGrammar(); 28 | } 29 | 30 | /** 31 | * Get the CouchDB database object. 32 | * 33 | * @return \Doctrine\CouchDB\CouchDBClient 34 | */ 35 | public function getCouchDBClient() 36 | { 37 | return $this->db; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getDriverName() 44 | { 45 | return 'couchdb'; 46 | } 47 | 48 | /** 49 | * Begin a fluent query against a database collection. 50 | * 51 | * @param string $collection 52 | * 53 | * @return Query\Builder 54 | */ 55 | public function collection($collection) 56 | { 57 | $query = new Query\Builder($this, $this->getPostProcessor()); 58 | 59 | return $query->from($collection); 60 | } 61 | 62 | /** 63 | * Begin a fluent query against a database collection. 64 | * 65 | * @param string $table 66 | * 67 | * @return Query\Builder 68 | */ 69 | public function table($table) 70 | { 71 | return $this->collection($table); 72 | } 73 | 74 | protected function getDefaultPostProcessor() 75 | { 76 | return new Query\Processor(); 77 | } 78 | 79 | protected function getDefaultQueryGrammar() 80 | { 81 | return new Query\Grammar(); 82 | } 83 | 84 | protected function getDefaultSchemaGrammar() 85 | { 86 | return new Schema\Grammar(); 87 | } 88 | 89 | public function getCollection($name) 90 | { 91 | return new Collection($this, $name); 92 | } 93 | 94 | /** 95 | * Dynamically pass methods to the connection. 96 | * 97 | * @param string $method 98 | * @param array $parameters 99 | * 100 | * @return mixed 101 | */ 102 | public function __call($method, $parameters) 103 | { 104 | return call_user_func_array([$this->db, $method], $parameters); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/CouchDBQueueServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('queue.failer', function ($app) { 16 | return new CouchFailedJobProvider($app['db'], config('queue.failed.database'), config('queue.failed.table')); 17 | }); 18 | } else { 19 | parent::registerFailedJobServices(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Eloquent/Builder.php: -------------------------------------------------------------------------------- 1 | model->getParentRelation()) { 22 | $relation->performUpdate($this->model, $values); 23 | return true; 24 | } 25 | 26 | return $this->query->update($this->addUpdatedAtColumn($values), $options); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function insert(array $values) 33 | { 34 | // Intercept operations on embedded models and delegate logic 35 | // to the parent relation instance. 36 | if ($relation = $this->model->getParentRelation()) { 37 | $relation->performInsert($this->model, $values); 38 | return true; 39 | } 40 | 41 | return parent::insert($values); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function insertGetId(array $values, $sequence = null) 48 | { 49 | // Intercept operations on embedded models and delegate logic 50 | // to the parent relation instance. 51 | if ($relation = $this->model->getParentRelation()) { 52 | $relation->performInsert($this->model, $values); 53 | 54 | return [$this->model->getKey(), null]; 55 | } 56 | 57 | return parent::insertGetId($values, $sequence); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function delete() 64 | { 65 | // Intercept operations on embedded models and delegate logic 66 | // to the parent relation instance. 67 | if ($relation = $this->model->getParentRelation()) { 68 | $relation->performDelete($this->model); 69 | 70 | return $this->model->getKey(); 71 | } 72 | 73 | return parent::delete(); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function raw($expression = null) 80 | { 81 | // Get raw results from the query builder. 82 | $results = $this->query->raw($expression); 83 | 84 | if (is_array($results) and array_key_exists('_id', $results)) { 85 | return $this->model->newFromBuilder((array) $results); 86 | } 87 | 88 | if ($results instanceof \Doctrine\CouchDB\HTTP\Response) { 89 | return $this->model->hydrate($results->body['docs']); 90 | } 91 | 92 | return $results; 93 | } 94 | 95 | /** 96 | * {@inheritdoc} 97 | */ 98 | public function increment($column, $amount = 1, array $extra = []) 99 | { 100 | // Intercept operations on embedded models and delegate logic 101 | // to the parent relation instance. 102 | if ($relation = $this->model->getParentRelation()) { 103 | $value = $this->model->{$column}; 104 | 105 | // When doing increment and decrements, Eloquent will automatically 106 | // sync the original attributes. We need to change the attribute 107 | // temporary in order to trigger an update query. 108 | $this->model->{$column} = null; 109 | 110 | $this->model->syncOriginalAttribute($column); 111 | 112 | $result = $this->model->update([$column => $value]); 113 | 114 | return $result; 115 | } 116 | 117 | return parent::increment($column, $amount, $extra); 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function decrement($column, $amount = 1, array $extra = []) 124 | { 125 | // Intercept operations on embedded models and delegate logic 126 | // to the parent relation instance. 127 | if ($relation = $this->model->getParentRelation()) { 128 | $value = $this->model->{$column}; 129 | 130 | // When doing increment and decrements, Eloquent will automatically 131 | // sync the original attributes. We need to change the attribute 132 | // temporary in order to trigger an update query. 133 | $this->model->{$column} = null; 134 | 135 | $this->model->syncOriginalAttribute($column); 136 | 137 | return $this->model->update([$column => $value]); 138 | } 139 | 140 | return parent::decrement($column, $amount, $extra); 141 | } 142 | 143 | public function push($column, $values, $unique = false) 144 | { 145 | return $this->query->push($column, $values, $unique); 146 | } 147 | 148 | public function pull($column, $values) 149 | { 150 | return $this->query->pull($column, $values); 151 | } 152 | 153 | public function getMangoQuery() 154 | { 155 | return $this->query->getMangoQuery(); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | protected function addHasWhere(EloquentBuilder $hasQuery, Relation $relation, $operator, $count, $boolean) 162 | { 163 | $query = $hasQuery->getQuery(); 164 | 165 | // Get the number of related objects for each possible parent. 166 | $relations = $query->pluck($relation->getHasCompareKey()); 167 | $relationCount = array_count_values(array_map(function ($id) { 168 | return (string) $id; // Convert Back ObjectIds to Strings 169 | }, is_array($relations) ? $relations : $relations->flatten()->toArray())); 170 | 171 | // Remove unwanted related objects based on the operator and count. 172 | $relationCount = array_filter($relationCount, function ($counted) use ($count, $operator) { 173 | // If we are comparing to 0, we always need all results. 174 | if ($count == 0) { 175 | return true; 176 | } 177 | 178 | switch ($operator) { 179 | case '>=': 180 | case '<': 181 | return $counted >= $count; 182 | case '>': 183 | case '<=': 184 | return $counted > $count; 185 | case '=': 186 | case '!=': 187 | return $counted == $count; 188 | } 189 | }); 190 | 191 | // If the operator is <, <= or !=, we will use whereNotIn. 192 | $not = in_array($operator, ['<', '<=', '!=']); 193 | 194 | // If we are comparing to 0, we need an additional $not flip. 195 | if ($count == 0) { 196 | $not = !$not; 197 | } 198 | 199 | // All related ids. 200 | $relatedIds = array_keys($relationCount); 201 | 202 | // Add whereIn to the query. 203 | return $this->whereIn($this->model->getKeyName(), $relatedIds, $boolean, $not); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Eloquent/EmbedsRelations.php: -------------------------------------------------------------------------------- 1 | newQuery(); 41 | //$query2 = $this->newQuery(); 42 | 43 | return new EmbedsMany($query, $this, $instance, $localKey, $foreignKey, $relation); 44 | } 45 | 46 | /** 47 | * Define an embedded one-to-many relationship. 48 | * 49 | * @param string $related 50 | * @param string $localKey 51 | * @param string $foreignKey 52 | * @param string $relation 53 | * 54 | * @return \Robsonvn\CouchDB\Relations\EmbedsOne 55 | */ 56 | protected function embedsOne($related, $localKey = null, $foreignKey = null, $relation = null) 57 | { 58 | // If no relation name was given, we will use this debug backtrace to extract 59 | // the calling method's name and use that as the relationship name as most 60 | // of the time this will be what we desire to use for the relationships. 61 | if (is_null($relation)) { 62 | list(, $caller) = debug_backtrace(false); 63 | 64 | $relation = $caller['function']; 65 | } 66 | 67 | if (is_null($localKey)) { 68 | $localKey = $relation; 69 | } 70 | 71 | if (is_null($foreignKey)) { 72 | $foreignKey = snake_case(class_basename($this)); 73 | } 74 | 75 | $query = $this->newQuery(); 76 | 77 | $instance = new $related(); 78 | 79 | return new EmbedsOne($query, $this, $instance, $localKey, $foreignKey, $relation); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Eloquent/HybridRelations.php: -------------------------------------------------------------------------------- 1 | getForeignKey(); 34 | 35 | $instance = new $related(); 36 | 37 | $localKey = $localKey ?: $this->getKeyName(); 38 | 39 | return new HasOne($instance->newQuery(), $this, $foreignKey, $localKey); 40 | } 41 | 42 | /** 43 | * Define a polymorphic one-to-one relationship. 44 | * 45 | * @param string $related 46 | * @param string $name 47 | * @param string $type 48 | * @param string $id 49 | * @param string $localKey 50 | * 51 | * @return \Illuminate\Database\Eloquent\Relations\MorphOne 52 | */ 53 | public function morphOne($related, $name, $type = null, $id = null, $localKey = null) 54 | { 55 | // Check if it is a relation with an original model. 56 | if (!is_subclass_of($related, \Robsonvn\CouchDB\Eloquent\Model::class)) { 57 | return parent::morphOne($related, $name, $type, $id, $localKey); 58 | } 59 | 60 | $instance = new $related(); 61 | 62 | list($type, $id) = $this->getMorphs($name, $type, $id); 63 | 64 | $localKey = $localKey ?: $this->getKeyName(); 65 | 66 | return new MorphOne($instance->newQuery(), $this, $type, $id, $localKey); 67 | } 68 | 69 | public function morphToMany($related, $name, $table = null, $foreignKey = null, $relatedKey = null, $inverse = false) 70 | { 71 | // Check if it is a relation with an original model. 72 | if (!is_subclass_of($related, \Robsonvn\CouchDB\Eloquent\Model::class)) { 73 | return parent::morphToMany($related, $name, $table, $foreignKey, $relatedKey, $inverse); 74 | } 75 | 76 | $caller = $this->guessBelongsToManyRelation(); 77 | 78 | // First, we will need to determine the foreign key and "other key" for the 79 | // relationship. Once we have determined the keys we will make the query 80 | // instances, as well as the relationship instances we need for these. 81 | $instance = $this->newRelatedInstance($related); 82 | 83 | $foreignKey = $foreignKey ?: $name.'_id'; 84 | 85 | $relatedKey = $relatedKey ?: $instance->getForeignKey(); 86 | 87 | // Now we're ready to create a new query builder for this related model and 88 | // the relationship instances for this relation. This relations will set 89 | // appropriate query constraints then entirely manages the hydrations. 90 | $table = $table ?: Str::plural($name); 91 | 92 | return new MorphToMany( 93 | $instance->newQuery(), $this, $name, $table, 94 | $foreignKey, $relatedKey, $caller, $inverse 95 | ); 96 | } 97 | 98 | /** 99 | * Define a one-to-many relationship. 100 | * 101 | * @param string $related 102 | * @param string $foreignKey 103 | * @param string $localKey 104 | * 105 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 106 | */ 107 | public function hasMany($related, $foreignKey = null, $localKey = null) 108 | { 109 | // Check if it is a relation with an original model. 110 | if (!is_subclass_of($related, \Robsonvn\CouchDB\Eloquent\Model::class)) { 111 | return parent::hasMany($related, $foreignKey, $localKey); 112 | } 113 | 114 | $foreignKey = $foreignKey ?: $this->getForeignKey(); 115 | 116 | $instance = new $related(); 117 | 118 | $localKey = $localKey ?: $this->getKeyName(); 119 | 120 | return new HasMany($instance->newQuery(), $this, $foreignKey, $localKey); 121 | } 122 | 123 | /** 124 | * Define a polymorphic one-to-many relationship. 125 | * 126 | * @param string $related 127 | * @param string $name 128 | * @param string $type 129 | * @param string $id 130 | * @param string $localKey 131 | * 132 | * @return \Illuminate\Database\Eloquent\Relations\MorphMany 133 | */ 134 | public function morphMany($related, $name, $type = null, $id = null, $localKey = null) 135 | { 136 | // Check if it is a relation with an original model. 137 | if (!is_subclass_of($related, \Robsonvn\CouchDB\Eloquent\Model::class)) { 138 | return parent::morphMany($related, $name, $type, $id, $localKey); 139 | } 140 | 141 | $instance = new $related(); 142 | 143 | // Here we will gather up the morph type and ID for the relationship so that we 144 | // can properly query the intermediate table of a relation. Finally, we will 145 | // get the table and create the relationship instances for the developers. 146 | list($type, $id) = $this->getMorphs($name, $type, $id); 147 | 148 | $table = $instance->getTable(); 149 | 150 | $localKey = $localKey ?: $this->getKeyName(); 151 | 152 | return new MorphMany($instance->newQuery(), $this, $type, $id, $localKey); 153 | } 154 | 155 | /** 156 | * Define an inverse one-to-one or many relationship. 157 | * 158 | * @param string $related 159 | * @param string $foreignKey 160 | * @param string $otherKey 161 | * @param string $relation 162 | * 163 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 164 | */ 165 | public function belongsTo($related, $foreignKey = null, $otherKey = null, $relation = null) 166 | { 167 | // If no relation name was given, we will use this debug backtrace to extract 168 | // the calling method's name and use that as the relationship name as most 169 | // of the time this will be what we desire to use for the relationships. 170 | if (is_null($relation)) { 171 | list($current, $caller) = debug_backtrace(false, 2); 172 | 173 | $relation = $caller['function']; 174 | } 175 | 176 | // Check if it is a relation with an original model. 177 | if (!is_subclass_of($related, \Robsonvn\CouchDB\Eloquent\Model::class)) { 178 | return parent::belongsTo($related, $foreignKey, $otherKey, $relation); 179 | } 180 | 181 | // If no foreign key was supplied, we can use a backtrace to guess the proper 182 | // foreign key name by using the name of the relationship function, which 183 | // when combined with an "_id" should conventionally match the columns. 184 | if (is_null($foreignKey)) { 185 | $foreignKey = Str::snake($relation).'_id'; 186 | } 187 | 188 | $instance = new $related(); 189 | 190 | // Once we have the foreign key names, we'll just create a new Eloquent query 191 | // for the related models and returns the relationship instance which will 192 | // actually be responsible for retrieving and hydrating every relations. 193 | $query = $instance->newQuery(); 194 | 195 | $otherKey = $otherKey ?: $instance->getKeyName(); 196 | 197 | return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); 198 | } 199 | 200 | /** 201 | * Define a polymorphic, inverse one-to-one or many relationship. 202 | * 203 | * @param string $name 204 | * @param string $type 205 | * @param string $id 206 | * 207 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo 208 | */ 209 | public function morphTo($name = null, $type = null, $id = null) 210 | { 211 | // If no name is provided, we will use the backtrace to get the function name 212 | // since that is most likely the name of the polymorphic interface. We can 213 | // use that to get both the class and foreign key that will be utilized. 214 | if (is_null($name)) { 215 | list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 216 | 217 | $name = Str::snake($caller['function']); 218 | } 219 | 220 | list($type, $id) = $this->getMorphs($name, $type, $id); 221 | 222 | // If the type value is null it is probably safe to assume we're eager loading 223 | // the relationship. When that is the case we will pass in a dummy query as 224 | // there are multiple types in the morph and we can't use single queries. 225 | if (is_null($class = $this->$type)) { 226 | return new MorphTo( 227 | $this->newQuery(), $this, $id, null, $type, $name 228 | ); 229 | } 230 | 231 | // If we are not eager loading the relationship we will essentially treat this 232 | // as a belongs-to style relationship since morph-to extends that class and 233 | // we will pass in the appropriate values so that it behaves as expected. 234 | else { 235 | $class = $this->getActualClassNameForMorph($class); 236 | 237 | $instance = new $class(); 238 | 239 | return new MorphTo( 240 | $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name 241 | ); 242 | } 243 | } 244 | 245 | /** 246 | * Define a many-to-many relationship. 247 | * 248 | * @param string $related 249 | * @param string $collection 250 | * @param string $foreignKey 251 | * @param string $otherKey 252 | * @param string $relation 253 | * 254 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany 255 | */ 256 | public function belongsToMany($related, $collection = null, $foreignPivotKey = null, $relatedPivotKey = NULL, $parentKey = NULL, $relatedKey = null, $relation = null) 257 | { 258 | // If no relationship name was passed, we will pull backtraces to get the 259 | // name of the calling function. We will use that function name as the 260 | // title of this relation since that is a great convention to apply. 261 | if (is_null($relation)) { 262 | $relation = $this->guessBelongsToManyRelation(); 263 | } 264 | 265 | // Check if it is a relation with an original model. 266 | if (!is_subclass_of($related, \Robsonvn\CouchDB\Eloquent\Model::class)) { 267 | return parent::belongsToMany($related, $collection, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relation); 268 | } 269 | 270 | // First, we'll need to determine the foreign key and "other key" for the 271 | // relationship. Once we have determined the keys we'll make the query 272 | // instances as well as the relationship instances we need for this. 273 | $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey().'s'; 274 | 275 | $instance = new $related(); 276 | 277 | $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey().'s'; 278 | 279 | // If no table name was provided, we can guess it by concatenating the two 280 | // models using underscores in alphabetical order. The two model names 281 | // are transformed to snake case from their default CamelCase also. 282 | if (is_null($collection)) { 283 | $collection = $instance->getTable(); 284 | } 285 | 286 | // Now we're ready to create a new query builder for the related model and 287 | // the relationship instances for the relation. The relations will set 288 | // appropriate query constraint and entirely manages the hydrations. 289 | $query = $instance->newQuery(); 290 | 291 | return new BelongsToMany($query, $this, $collection, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relation); 292 | } 293 | 294 | /** 295 | * Get the relationship name of the belongs to many. 296 | * 297 | * @return string 298 | */ 299 | protected function guessBelongsToManyRelation() 300 | { 301 | if (method_exists($this, 'getBelongsToManyCaller')) { 302 | return $this->getBelongsToManyCaller(); 303 | } 304 | 305 | return parent::guessBelongsToManyRelation(); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/Eloquent/Model.php: -------------------------------------------------------------------------------- 1 | primaryKey !== '_id') { 44 | throw new \Exception('CouchDB primary key must be _id', 1); 45 | } 46 | parent::__construct($attributes); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function newEloquentBuilder($query) 53 | { 54 | return new Builder($query); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | protected function newBaseQueryBuilder() 61 | { 62 | $connection = $this->getConnection(); 63 | 64 | return new QueryBuilder($connection, $connection->getPostProcessor()); 65 | } 66 | 67 | public function getRevisionAttributeName() 68 | { 69 | return $this->revisionAttributeName; 70 | } 71 | 72 | public function getRevision() 73 | { 74 | $attr = $this->getRevisionAttributeName(); 75 | 76 | return $this->$attr; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | protected function setKeysForSaveQuery(BaseBuilder $query) 83 | { 84 | 85 | $query->where($this->getKeyName(), '=', $this->getKeyForSaveQuery()); 86 | 87 | if ($this->getRevision()) { 88 | $query->where($this->getRevisionAttributeName(), '=', $this->getRevision()); 89 | } 90 | 91 | $query->orderBy('_id')->orderBy('_rev'); 92 | 93 | return $query; 94 | } 95 | 96 | /** 97 | * Custom accessor for the model's id. 98 | * 99 | * @param mixed $value 100 | * 101 | * @return mixed 102 | */ 103 | public function getIdAttribute($value = null) 104 | { 105 | if (!$value and array_key_exists('_id', $this->attributes)) { 106 | $value = $this->attributes['_id']; 107 | } 108 | 109 | return $value; 110 | } 111 | 112 | public function setIdAttribute($value = null) 113 | { 114 | $this->attributes['_id'] = $value; 115 | 116 | return $value; 117 | } 118 | 119 | /** 120 | * Set the parent relation. 121 | * 122 | * @param \Illuminate\Database\Eloquent\Relations\Relation $relation 123 | */ 124 | public function setParentRelation(Model $parent, $relation) 125 | { 126 | $this->parent = $parent; 127 | $this->parentRelation = $relation; 128 | } 129 | 130 | /** 131 | * Get the parent relation. 132 | * 133 | * @return \Illuminate\Database\Eloquent\Relations\Relation 134 | */ 135 | public function getParentRelation() 136 | { 137 | if(isset($this->parentRelation)){ 138 | $relation = call_user_func_array([$this->parent,$this->parentRelation],[]); 139 | return $relation; 140 | } 141 | return; 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function getTable() 148 | { 149 | return $this->collection ?: parent::getTable(); 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function getQualifiedKeyName() 156 | { 157 | return $this->getKeyName(); 158 | } 159 | 160 | /** 161 | * {@inheritdoc} 162 | * Perform a model update operation. 163 | * 164 | * @param \Illuminate\Database\Eloquent\Builder $query 165 | * 166 | * @return bool 167 | */ 168 | protected function performUpdate(BaseBuilder $query) 169 | { 170 | // If the updating event returns false, we will cancel the update operation so 171 | // developers can hook Validation systems into their models and cancel this 172 | // operation if the model does not pass validation. Otherwise, we update. 173 | if ($this->fireModelEvent('updating') === false) { 174 | return false; 175 | } 176 | 177 | // First we need to create a fresh query instance and touch the creation and 178 | // update timestamp on the model which are maintained by us for developer 179 | // convenience. Then we will just continue saving the model instances. 180 | if ($this->usesTimestamps()) { 181 | $this->updateTimestamps(); 182 | } 183 | 184 | // Once we have run the update operation, we will fire the "updated" event for 185 | // this model instance. This will allow developers to hook into these after 186 | // models are updated, giving them a chance to do any special processing. 187 | $attributes = $this->getAttributes(); 188 | 189 | if ($this->isDirty()) { 190 | $response = $this->setKeysForSaveQuery($query)->update($attributes); 191 | 192 | if (is_array($response) && array_key_exists(0, $response) && array_key_exists('rev', $response[0])) { 193 | $this->setAttribute($this->getRevisionAttributeName(), $response[0]['rev']); 194 | } 195 | 196 | $this->fireModelEvent('updated', false); 197 | } 198 | 199 | return true; 200 | } 201 | 202 | /** 203 | * Perform a model insert operation. 204 | * 205 | * @param \Illuminate\Database\Eloquent\Builder $query 206 | * 207 | * @return bool 208 | */ 209 | protected function performInsert(BaseBuilder $query) 210 | { 211 | if ($this->fireModelEvent('creating') === false) { 212 | return false; 213 | } 214 | 215 | // First we'll need to create a fresh query instance and touch the creation and 216 | // update timestamps on this model, which are maintained by us for developer 217 | // convenience. After, we will just continue saving these model instances. 218 | if ($this->usesTimestamps()) { 219 | $this->updateTimestamps(); 220 | } 221 | 222 | //If model uses softDeletes, lets force the deleted column as null 223 | if (method_exists($this, 'getQualifiedDeletedAtColumn')) { 224 | $this->setAttribute($this->getQualifiedDeletedAtColumn(), null); 225 | } 226 | 227 | // If the model has an incrementing key, we can use the "insertGetId" method on 228 | // the query builder, which will give us back the final inserted ID for this 229 | // table from the database. Not all tables have to be incrementing though. 230 | $attributes = $this->getAttributes(); 231 | 232 | $keyName = $this->getKeyName(); 233 | 234 | if ($this->getIncrementing() && !isset($attributes['id'])) { 235 | list($id, $rev) = $query->insertGetId($attributes, $keyName); 236 | } 237 | 238 | // If the table isn't incrementing we'll simply insert these attributes as they 239 | // are. These attribute arrays must contain an "id" column previously placed 240 | // there by the developer as the manually determined key for these models. 241 | else { 242 | if (empty($attributes)) { 243 | return true; 244 | } 245 | $response = $query->insert($attributes); 246 | 247 | if (count($response) !== 1) { 248 | return false; 249 | } 250 | 251 | $id = $response[0]['id']; 252 | $rev = $response[0]['rev']; 253 | } 254 | 255 | $this->setAttribute($keyName, $id); 256 | if ($rev) { 257 | $this->setAttribute($this->getRevisionAttributeName(), $rev); 258 | } 259 | 260 | // We will go ahead and set the exists property to true, so that it is set when 261 | // the created event is fired, just in case the developer tries to update it 262 | // during the event. This will allow them to do so and run an update here. 263 | $this->exists = true; 264 | 265 | $this->wasRecentlyCreated = true; 266 | 267 | $this->fireModelEvent('created', false); 268 | 269 | return true; 270 | } 271 | 272 | /** 273 | * {@inheritdoc} 274 | */ 275 | public function getAttribute($key) 276 | { 277 | if (!$key) { 278 | return; 279 | } 280 | 281 | // Dot notation support. 282 | if (str_contains($key, '.') and array_has($this->attributes, $key)) { 283 | return $this->getAttributeValue($key); 284 | } 285 | 286 | // This checks for embedded relation support. 287 | if (method_exists($this, $key) and !method_exists(self::class, $key)) { 288 | return $this->getRelationValue($key); 289 | } 290 | 291 | return parent::getAttribute($key); 292 | } 293 | 294 | /** 295 | * {@inheritdoc} 296 | */ 297 | protected function getAttributeFromArray($key) 298 | { 299 | //TODO add suport to get attribute with cast using doting notation 300 | // Support keys in dot notation. 301 | if (str_contains($key, '.')) { 302 | return array_get($this->attributes, $key); 303 | } 304 | 305 | return parent::getAttributeFromArray($key); 306 | } 307 | 308 | public function applyCastArrayRecursive($key, $value) 309 | { 310 | if (is_array($value)) { 311 | $is_sequencial = array_keys($value) === range(0, count($value) - 1); 312 | 313 | foreach ($value as $subkey=> &$item) { 314 | //create a dot notation for the key, ignore subkey if is a sequencial array 315 | $tree = $key.(($is_sequencial) ? '' : '.'.$subkey); 316 | 317 | $item = $this->applyCastArrayRecursive($tree, $item); 318 | } 319 | 320 | return $value; 321 | } else { 322 | return $this->applyCasts($key, $value); 323 | } 324 | } 325 | 326 | protected function applyCasts($key, $value) 327 | { 328 | //Date cast 329 | if (in_array($key, $this->getDates()) && $value) { 330 | $value = $this->fromDateTime($value); 331 | } 332 | 333 | return $value; 334 | } 335 | 336 | /** 337 | * {@inheritdoc} 338 | */ 339 | public function setAttribute($key, $value) 340 | { 341 | if (is_array($value)) { 342 | $value = $this->applyCastArrayRecursive($key, $value); 343 | } 344 | 345 | if (str_contains($key, '.')) { 346 | $value = $this->applyCasts($key, $value); 347 | array_set($this->attributes, $key, $value); 348 | 349 | return; 350 | } else { 351 | parent::setAttribute($key, $value); 352 | } 353 | } 354 | 355 | /** 356 | * {@inheritdoc} 357 | */ 358 | public function getCasts() 359 | { 360 | return $this->casts; 361 | } 362 | 363 | /** 364 | * {@inheritdoc} 365 | */ 366 | public function attributesToArray() 367 | { 368 | $attributes = parent::attributesToArray(); 369 | 370 | // Convert dot-notation dates. 371 | foreach ($this->getDates() as $key) { 372 | if (str_contains($key, '.') and array_has($attributes, $key)) { 373 | array_set($attributes, $key, (string) $this->asDateTime(array_get($attributes, $key))); 374 | } 375 | } 376 | 377 | return $attributes; 378 | } 379 | 380 | /** 381 | * {@inheritdoc} 382 | */ 383 | protected function removeTableFromKey($key) 384 | { 385 | return $key; 386 | } 387 | 388 | public function drop($columns) 389 | { 390 | if (!$this->exists) { 391 | return; 392 | } 393 | 394 | if (!is_array($columns)) { 395 | $columns = [$columns]; 396 | } 397 | 398 | foreach ($columns as $column) { 399 | $this->__unset($column); 400 | } 401 | 402 | return $this->newQuery()->where( 403 | [ 404 | $this->getKeyName() => $this->getKey(), 405 | $this->getRevisionAttributeName() => $this->getRevision(), 406 | ])->unset($columns); 407 | } 408 | 409 | public function fromDateTime($value) 410 | { 411 | return $this->asDateTime($value)->format( 412 | 'Y-m-d H:i:s' 413 | ); 414 | } 415 | 416 | protected function asDateTime($value) 417 | { 418 | if (is_string($value) && $this->isStandardCouchDBDateFormat($value)) { 419 | return Carbon::createFromFormat('Y-m-d H:i:s', $value); 420 | } 421 | 422 | return parent::asDateTime($value); 423 | } 424 | 425 | protected function getDateFormat() 426 | { 427 | return $this->dateFormat ?: 'Y-m-d H:i:s'; 428 | } 429 | 430 | /** 431 | * Determine if the given value is a standard CouchDB date format. 432 | * 433 | * @param string $value 434 | * 435 | * @return bool 436 | */ 437 | protected function isStandardCouchDBDateFormat($value) 438 | { 439 | return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/', $value); 440 | } 441 | 442 | /** 443 | * {@inheritdoc} 444 | */ 445 | public function push() 446 | { 447 | if ($parameters = func_get_args()) { 448 | $unique = false; 449 | 450 | if (count($parameters) == 3) { 451 | list($column, $values, $unique) = $parameters; 452 | } else { 453 | list($column, $values) = $parameters; 454 | } 455 | 456 | // Do batch push by default. 457 | if (!is_array($values)) { 458 | $values = [$values]; 459 | } 460 | 461 | $query = $this->setKeysForSaveQuery($this->newQuery()); 462 | 463 | $this->pushAttributeValues($column, $values, $unique); 464 | 465 | $response = $query->push($column, $values, $unique); 466 | 467 | $this->attributes['_rev'] = $response[0]['rev']; 468 | $this->syncOriginalAttribute('_rev'); 469 | 470 | return $this; 471 | } 472 | 473 | return parent::push(); 474 | } 475 | 476 | /** 477 | * Remove one or more values from an array. 478 | * 479 | * @param string $column 480 | * @param mixed $values 481 | * 482 | * @return mixed 483 | */ 484 | public function pull($column, $values) 485 | { 486 | // Do batch pull by default. 487 | if (!is_array($values)) { 488 | $values = [$values]; 489 | } 490 | 491 | $query = $this->setKeysForSaveQuery($this->newQuery()); 492 | 493 | $this->pullAttributeValues($column, $values); 494 | 495 | $response = $query->pull($column, $values); 496 | 497 | $this->attributes['_rev'] = $response[0]['rev']; 498 | $this->syncOriginalAttribute('_rev'); 499 | 500 | return $this; 501 | } 502 | 503 | /** 504 | * Append one or more values to the underlying attribute value and sync with original. 505 | * 506 | * @param string $column 507 | * @param array $values 508 | * @param bool $unique 509 | */ 510 | protected function pushAttributeValues($column, array $values, $unique = false) 511 | { 512 | $current = $this->getAttributeFromArray($column) ?: []; 513 | 514 | foreach ($values as $value) { 515 | // Don't add duplicate values when we only want unique values. 516 | if ($unique and in_array($value, $current)) { 517 | continue; 518 | } 519 | 520 | array_push($current, $value); 521 | } 522 | 523 | $this->attributes[$column] = $current; 524 | 525 | $this->syncOriginalAttribute($column); 526 | } 527 | 528 | /** 529 | * Remove one or more values to the underlying attribute value and sync with original. 530 | * 531 | * @param string $column 532 | * @param array $values 533 | */ 534 | protected function pullAttributeValues($column, array $values) 535 | { 536 | $current = $this->getAttributeFromArray($column) ?: []; 537 | 538 | foreach ($values as $value) { 539 | $keys = array_keys($current, $value); 540 | 541 | foreach ($keys as $key) { 542 | unset($current[$key]); 543 | } 544 | } 545 | 546 | $this->attributes[$column] = array_values($current); 547 | 548 | $this->syncOriginalAttribute($column); 549 | } 550 | 551 | /** 552 | * {@inheritdoc} 553 | */ 554 | public function getForeignKey() 555 | { 556 | return Str::snake(class_basename($this)).'_'.ltrim($this->primaryKey, '_'); 557 | } 558 | 559 | /** 560 | * {@inheritdoc} 561 | */ 562 | public function fresh($with = []) 563 | { 564 | if (! $this->exists) { 565 | return; 566 | } 567 | 568 | return static::newQueryWithoutScopes() 569 | ->with(is_string($with) ? func_get_args() : $with) 570 | ->where($this->getKeyName(), $this->getKey()) 571 | ->orderBy($this->getKeyName()) 572 | ->first(); 573 | } 574 | 575 | /** 576 | * {@inheritdoc} 577 | */ 578 | public function __call($method, $parameters) 579 | { 580 | // Unset method 581 | if ($method == 'unset') { 582 | return call_user_func_array([$this, 'drop'], $parameters); 583 | } 584 | 585 | return parent::__call($method, $parameters); 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /src/Eloquent/SoftDeletes.php: -------------------------------------------------------------------------------- 1 | getDeletedAtColumn(); 15 | } 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | protected function runSoftDelete() 21 | { 22 | $query = $this->setKeysForSaveQuery($this->newQueryWithoutScopes()); 23 | 24 | $this->{$this->getDeletedAtColumn()} = $time = $this->freshTimestamp(); 25 | 26 | $query->update($this->getAttributes()); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function restore() 33 | { 34 | // If the restoring event does not return false, we will proceed with this 35 | // restore operation. Otherwise, we bail out so the developer will stop 36 | // the restore totally. We will clear the deleted timestamp and save. 37 | if ($this->fireModelEvent('restoring') === false) { 38 | return false; 39 | } 40 | 41 | $this->{$this->getDeletedAtColumn()} = null; 42 | 43 | // Once we have saved the model, we will fire the "restored" event so this 44 | // developer will do anything they need to after a restore operation is 45 | // totally finished. Then we will return the result of the save call. 46 | $this->exists = true; 47 | 48 | $result = $this->save(); 49 | 50 | $this->fireModelEvent('restored', false); 51 | 52 | return $result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exceptions/QueryException.php: -------------------------------------------------------------------------------- 1 | $value) { 12 | //if the key exists in the second array, recursively call this function 13 | //if it is an array, otherwise check if the value is in arr2 14 | if (array_key_exists($key, $arr2)) { 15 | if (is_array($value)) { 16 | $is_sequencial = (is_array($value) and array_keys($value) === range(0, count($value) - 1)); 17 | 18 | $recursiveDiff = self::array_diff_recursive($value, $arr2[$key]); 19 | 20 | if (count($recursiveDiff)) { 21 | //if is a sequencial array, reset array index 22 | if ($is_sequencial && !$keep_order) { 23 | $recursiveDiff = array_values($recursiveDiff); 24 | } 25 | $outputDiff[$key] = $recursiveDiff; 26 | } else { 27 | //if is a assoc array keep value as array even if empty 28 | if (is_string($key)) { 29 | $outputDiff[$key] = []; 30 | } 31 | } 32 | } elseif (!in_array($value, $arr2)) { 33 | $outputDiff[$key] = $value; 34 | } 35 | } 36 | //if the key is not in the second array, check if the value is in 37 | //the second array (this is a quirk of how array_diff works) 38 | elseif (!in_array($value, $arr2)) { 39 | $outputDiff[$key] = $value; 40 | } 41 | } 42 | 43 | return $outputDiff; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Passport/AuthCode.php: -------------------------------------------------------------------------------- 1 | 'bool', 30 | ]; 31 | 32 | /** 33 | * The attributes that should be mutated to dates. 34 | * 35 | * @var array 36 | */ 37 | protected $dates = [ 38 | 'expires_at', 39 | ]; 40 | 41 | /** 42 | * Get the client that owns the authentication code. 43 | * 44 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 45 | */ 46 | public function client() 47 | { 48 | return $this->hasMany(Client::class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Passport/Bridge/RefreshTokenRepository.php: -------------------------------------------------------------------------------- 1 | events = $events; 51 | $this->tokens = $tokens; 52 | $this->database = $database; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getNewRefreshToken() 59 | { 60 | return new RefreshToken; 61 | } 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity) 66 | { 67 | $this->database->table('oauth_refresh_tokens')->insert([ 68 | '_id' => $id = $refreshTokenEntity->getIdentifier(), 69 | 'access_token_id' => $accessTokenId = $refreshTokenEntity->getAccessToken()->getIdentifier(), 70 | 'revoked' => false, 71 | 'expires_at' => $refreshTokenEntity->getExpiryDateTime()->format('Y-m-d H:i:s'), 72 | ]); 73 | 74 | $this->events->fire(new RefreshTokenCreated($id, $accessTokenId)); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function revokeRefreshToken($tokenId) 81 | { 82 | $this->database->table('oauth_refresh_tokens') 83 | ->where('_id', $tokenId)->update(['revoked' => true]); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function isRefreshTokenRevoked($tokenId) 90 | { 91 | $refreshToken = $this->database->table('oauth_refresh_tokens') 92 | ->where('_id', $tokenId)->first(); 93 | 94 | if ($refreshToken === null || $refreshToken->revoked) { 95 | return true; 96 | } 97 | 98 | return $this->tokens->isAccessTokenRevoked( 99 | $refreshToken->access_token_id 100 | ); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Passport/Client.php: -------------------------------------------------------------------------------- 1 | 'bool', 39 | 'password_client' => 'bool', 40 | 'revoked' => 'bool', 41 | ]; 42 | 43 | /** 44 | * Get all of the authentication codes for the client. 45 | * 46 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 47 | */ 48 | public function authCodes() 49 | { 50 | return $this->hasMany(AuthCode::class); 51 | } 52 | 53 | /** 54 | * Get all of the tokens that belong to the client. 55 | * 56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 57 | */ 58 | public function tokens() 59 | { 60 | return $this->hasMany(Token::class); 61 | } 62 | 63 | /** 64 | * Determine if the client is a "first party" client. 65 | * 66 | * @return bool 67 | */ 68 | public function firstParty() 69 | { 70 | return $this->personal_access_client || $this->password_client; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Passport/PassportServiceProvider.php: -------------------------------------------------------------------------------- 1 | alias('Laravel\Passport\AuthCode', AuthCode::class); 17 | $loader->alias('Laravel\Passport\Client', Client::class); 18 | $loader->alias('Laravel\Passport\PersonalAccessClient', PersonalAccessClient::class); 19 | $loader->alias('Laravel\Passport\Token', Token::class); 20 | $loader->alias('Laravel\Passport\Bridge\RefreshTokenRepository', Bridge\RefreshTokenRepository::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Passport/PersonalAccessClient.php: -------------------------------------------------------------------------------- 1 | belongsTo(Client::class); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Passport/Token.php: -------------------------------------------------------------------------------- 1 | 'bool', 44 | ]; 45 | 46 | /** 47 | * The attributes that should be mutated to dates. 48 | * 49 | * @var array 50 | */ 51 | protected $dates = [ 52 | 'expires_at', 53 | ]; 54 | 55 | /** 56 | * Overwrite scopes setter to handle default passport JSON string 57 | * and save native array. 58 | * 59 | * @param mixed $scopes 60 | */ 61 | public function setScopesAttribute($scopes) 62 | { 63 | if (is_string($scopes)) { 64 | $scopes = json_decode($scopes, true); 65 | } 66 | 67 | // If successfully decoded into array, then it will be saved as array. 68 | // If still string, will be converted to array to preserve consistency. 69 | $this->attributes['scopes'] = (array) $scopes; 70 | } 71 | 72 | /** 73 | * Get the client that the token belongs to. 74 | * 75 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 76 | */ 77 | public function client() 78 | { 79 | return $this->belongsTo(Client::class); 80 | } 81 | 82 | /** 83 | * Determine if the token has a given scope. 84 | * 85 | * @param string $scope 86 | * 87 | * @return bool 88 | */ 89 | public function can($scope) 90 | { 91 | return in_array('*', $this->scopes) || 92 | array_key_exists($scope, array_flip($this->scopes)); 93 | } 94 | 95 | /** 96 | * Determine if the token is missing a given scope. 97 | * 98 | * @param string $scope 99 | * 100 | * @return bool 101 | */ 102 | public function cant($scope) 103 | { 104 | return !$this->can($scope); 105 | } 106 | 107 | /** 108 | * Revoke the token instance. 109 | * 110 | * @return void 111 | */ 112 | public function revoke() 113 | { 114 | $this->forceFill(['revoked' => true])->save(); 115 | } 116 | 117 | /** 118 | * Determine if the token is a transient JWT token. 119 | * 120 | * @return bool 121 | */ 122 | public function transient() 123 | { 124 | return false; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Query/Grammar.php: -------------------------------------------------------------------------------- 1 | connections = $connections; 25 | } 26 | 27 | /** 28 | * Establish a queue connection. 29 | * 30 | * @param array $config 31 | * @return \Illuminate\Contracts\Queue\Queue 32 | */ 33 | public function connect(array $config) 34 | { 35 | return new CouchQueue( 36 | $this->connections->connection(Arr::get($config, 'connection')), 37 | $config['table'], 38 | $config['queue'], 39 | Arr::get($config, 'expire', 60) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Queue/CouchJob.php: -------------------------------------------------------------------------------- 1 | job->reserved; 15 | } 16 | 17 | /** 18 | * @return \DateTime 19 | */ 20 | public function reservedAt() 21 | { 22 | return $this->job->reserved_at; 23 | } 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | public function release($delay = 0) 29 | { 30 | //Release failed job with 60 seconds of delay 31 | if($this->job->attempts>1){ 32 | $delay +=60; 33 | } 34 | parent::release($delay); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Queue/CouchQueue.php: -------------------------------------------------------------------------------- 1 | getQueue($queue); 15 | 16 | if (! is_null($this->retryAfter)) { 17 | $this->releaseJobsThatHaveBeenReservedTooLong($queue); 18 | } 19 | 20 | if ($job = $this->getNextAvailableJobAndReserve($queue)) { 21 | return new CouchJob( 22 | $this->container, $this, $job, $this->connectionName, $queue 23 | ); 24 | } 25 | } 26 | 27 | /** 28 | * Get the next available job for the queue and mark it as reserved. 29 | * 30 | * When using multiple daemon queue listeners to process jobs there 31 | * is a possibility that multiple processes can end up reading the 32 | * same record before one has flagged it as reserved. 33 | * 34 | * This race condition can result in random jobs being run more then 35 | * once. To solve this we'll try to update the document using the _rev 36 | * if it fails means that the _rev is outdate and we try to get 37 | * the next available job. 38 | * 39 | * @param string|null $queue 40 | * 41 | * @return \StdClass|null 42 | */ 43 | protected function getNextAvailableJobAndReserve($queue) 44 | { 45 | $job = $this->database->collection($this->table) 46 | ->where('queue', $this->getQueue($queue)) 47 | ->whereNull('reserved_at') 48 | ->where('available_at', '<=', Carbon::now()->getTimestamp())->first(); 49 | 50 | if (count($job)) { 51 | 52 | $job['reserved'] = 1; 53 | $job['attempts']++; 54 | $job['reserved_at'] = Carbon::now()->getTimestamp(); 55 | 56 | try{ 57 | list($_id, $_rev) = $this->database->getCollection($this->table)->putDocument($job, $job['_id'],$job['_rev']); 58 | 59 | $job['_rev'] = $_rev; 60 | $job = (object) $job; 61 | $job->id = $job->_id; 62 | 63 | }catch(\Doctrine\CouchDB\HTTP\HTTPException $e){ 64 | $job = $this->getNextAvailableJobAndReserve($queue); 65 | } 66 | } 67 | return $job; 68 | } 69 | 70 | /** 71 | * Release the jobs that have been reserved for too long. 72 | * 73 | * @param string $queue 74 | * @return void 75 | */ 76 | protected function releaseJobsThatHaveBeenReservedTooLong($queue) 77 | { 78 | $expiration = Carbon::now()->subSeconds($this->retryAfter)->getTimestamp(); 79 | 80 | $reserved = $this->database->collection($this->table) 81 | ->where('queue', $this->getQueue($queue)) 82 | ->where('reserved_at', '<=', $expiration)->get(); 83 | 84 | foreach ($reserved as $job) { 85 | $this->database->table($this->table)->where('_id', $job['_id'])->update([ 86 | 'reserved' => 0, 87 | 'reserved_at' => null, 88 | ]); 89 | } 90 | } 91 | 92 | 93 | /** 94 | * @inheritdoc 95 | */ 96 | public function deleteReserved($queue, $id) 97 | { 98 | $this->database->collection($this->table)->where('_id', $id)->delete(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Queue/Failed/CouchFailedJobProvider.php: -------------------------------------------------------------------------------- 1 | getTimestamp(); 20 | 21 | $this->getTable()->insert(compact('connection', 'queue', 'payload', 'failed_at', 'exception')); 22 | } 23 | 24 | /** 25 | * Get a list of all of the failed jobs. 26 | * 27 | * @return array 28 | */ 29 | public function all() 30 | { 31 | $all = $this->getTable()->orderBy('_id', 'desc')->get()->all(); 32 | 33 | $all = array_map(function ($job) { 34 | $job['id'] = (string) $job['_id']; 35 | return $job; 36 | }, $all); 37 | 38 | return $all; 39 | } 40 | 41 | /** 42 | * Get a single failed job. 43 | * 44 | * @param mixed $id 45 | * @return array 46 | */ 47 | public function find($id) 48 | { 49 | $job = $this->getTable()->find($id); 50 | 51 | $job = (object) $job; 52 | $job->id = $job->_id; 53 | 54 | return $job; 55 | } 56 | 57 | /** 58 | * Delete a single failed job from storage. 59 | * 60 | * @param mixed $id 61 | * @return bool 62 | */ 63 | public function forget($id) 64 | { 65 | return $this->getTable()->where('_id', $id)->delete() > 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Relations/BelongsTo.php: -------------------------------------------------------------------------------- 1 | query->where($this->getOwnerKey(), '=', $this->parent->{$this->foreignKey}); 19 | //Force index 20 | $this->query->orderBy($this->getOwnerKey()); 21 | } 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function addEagerConstraints(array $models) 28 | { 29 | // We'll grab the primary key name of the related models since it could be set to 30 | // a non-standard name and not "id". We will then construct the constraint for 31 | // our eagerly loading query so it returns the proper models from execution. 32 | $key = $this->getOwnerKey(); 33 | $eager_keys = $this->getEagerModelKeys($models); 34 | 35 | if($eager_keys === [null]){ 36 | $eager_keys = []; 37 | } 38 | 39 | $this->query->whereIn($key, $eager_keys); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 46 | { 47 | return $query; 48 | } 49 | 50 | /** 51 | * Get the owner key with backwards compatible support. 52 | * 53 | * @return string 54 | */ 55 | public function getOwnerKey() 56 | { 57 | return property_exists($this, 'ownerKey') ? $this->ownerKey : $this->otherKey; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Relations/BelongsToMany.php: -------------------------------------------------------------------------------- 1 | getForeignKey(); 20 | } 21 | 22 | public function getQualifiedForeignPivotKeyName() 23 | { 24 | return $this->foreignKey; 25 | } 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 30 | { 31 | return $query; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function hydratePivotRelation(array $models) 38 | { 39 | // Do nothing. 40 | } 41 | 42 | /** 43 | * Set the select clause for the relation query. 44 | * 45 | * @param array $columns 46 | * 47 | * @return array 48 | */ 49 | protected function getSelectColumns(array $columns = ['*']) 50 | { 51 | return $columns; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function shouldSelect(array $columns = ['*']) 58 | { 59 | return $columns; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function addConstraints() 66 | { 67 | if (static::$constraints) { 68 | $this->setWhere(); 69 | } 70 | } 71 | 72 | /** 73 | * Set the where clause for the relation query. 74 | * 75 | * @return $this 76 | */ 77 | protected function setWhere() 78 | { 79 | $foreign = $this->getForeignKey(); 80 | 81 | $this->query->whereIn($foreign, [$this->parent->getKey()]); 82 | //Force index 83 | $this->query->orderBy($foreign); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function save(Model $model, array $joining = [], $touch = true) 92 | { 93 | $model->save(['touch' => false]); 94 | 95 | $this->attach($model, $joining, $touch); 96 | 97 | return $model; 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function create(array $attributes = [], array $joining = [], $touch = true) 104 | { 105 | $instance = $this->related->newInstance($attributes); 106 | 107 | // Once we save the related model, we need to attach it to the base model via 108 | // through intermediate table so we'll use the existing "attach" method to 109 | // accomplish this which will insert the record and any more attributes. 110 | $instance->save(['touch' => false]); 111 | 112 | $this->attach($instance, $joining, $touch); 113 | 114 | return $instance; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function sync($ids, $detaching = true) 121 | { 122 | $changes = [ 123 | 'attached' => [], 124 | 'detached' => [], 125 | 'updated' => [], 126 | ]; 127 | 128 | if ($ids instanceof Collection) { 129 | $ids = $ids->modelKeys(); 130 | } 131 | 132 | // First we need to attach any of the associated models that are not currently 133 | // in this joining table. We'll spin through the given IDs, checking to see 134 | // if they exist in the array of current ones, and if not we will insert. 135 | $current = $this->parent->{$this->getRelatedKey()} ?: []; 136 | 137 | // See issue #256. 138 | if ($current instanceof Collection) { 139 | $current = $ids->modelKeys(); 140 | } 141 | 142 | $records = $this->formatSyncList($ids); 143 | 144 | 145 | $detach = array_diff($current, array_keys($records)); 146 | 147 | // We need to make sure we pass a clean array, so that it is not interpreted 148 | // as an associative array. 149 | $detach = array_values($detach); 150 | 151 | // Next, we will take the differences of the currents and given IDs and detach 152 | // all of the entities that exist in the "current" array but are not in the 153 | // the array of the IDs given to the method which will complete the sync. 154 | if ($detaching and count($detach) > 0) { 155 | $this->detach($detach); 156 | 157 | $changes['detached'] = (array) array_map(function ($v) { 158 | return is_numeric($v) ? (int) $v : (string) $v; 159 | }, $detach); 160 | } 161 | 162 | // Now we are finally ready to attach the new records. Note that we'll disable 163 | // touching until after the entire operation is complete so we don't fire a 164 | // ton of touch operations until we are totally done syncing the records. 165 | $changes = array_merge( 166 | $changes, $this->attachNew($records, $current, false) 167 | ); 168 | 169 | if (count($changes['attached']) || count($changes['updated'])) { 170 | $this->touchIfTouching(); 171 | } 172 | 173 | return $changes; 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function updateExistingPivot($id, array $attributes, $touch = true) 180 | { 181 | // Do nothing, we have no pivot table. 182 | } 183 | 184 | /** 185 | * {@inheritdoc} 186 | */ 187 | public function attach($id, array $attributes = [], $touch = true) 188 | { 189 | if ($id instanceof Model) { 190 | $model = $id; 191 | 192 | $id = $model->getKey(); 193 | 194 | // Attach the new parent id to the related model. 195 | $model->push($this->foreignKey, $this->parent->getKey(), true); 196 | } else { 197 | if ($id instanceof Collection) { 198 | $id = $id->modelKeys(); 199 | } 200 | 201 | $query = $this->newRelatedQuery(); 202 | 203 | $query->whereIn($this->related->getKeyName(), (array) $id); 204 | 205 | // Attach the new parent id to the related model. 206 | $query->push($this->foreignKey, $this->parent->getKey(), true); 207 | } 208 | 209 | // Attach the new ids to the parent model. 210 | $this->parent->push($this->getRelatedKey(), (array) $id, true); 211 | 212 | if ($touch) { 213 | $this->touchIfTouching(); 214 | } 215 | } 216 | 217 | /** 218 | * {@inheritdoc} 219 | */ 220 | public function detach($ids = [], $touch = true) 221 | { 222 | if ($ids instanceof Model) { 223 | $ids = (array) $ids->getKey(); 224 | } else if ($ids instanceof Collection) { 225 | $ids = $ids->modelKeys(); 226 | } 227 | 228 | $query = $this->newRelatedQuery(); 229 | 230 | // If associated IDs were passed to the method we will only delete those 231 | // associations, otherwise all of the association ties will be broken. 232 | // We'll return the numbers of affected rows when we do the deletes. 233 | $ids = (array) $ids; 234 | 235 | // Detach all ids from the parent model. 236 | $this->parent->pull($this->getRelatedKey(), $ids); 237 | 238 | // Prepare the query to select all related objects. 239 | if (count($ids) > 0) { 240 | $query->whereIn($this->related->getKeyName(), $ids); 241 | } 242 | 243 | // Remove the relation to the parent. 244 | $query->pull($this->foreignKey, $this->parent->getKey()); 245 | 246 | if ($touch) { 247 | $this->touchIfTouching(); 248 | } 249 | 250 | return count($ids); 251 | } 252 | 253 | /** 254 | * {@inheritdoc} 255 | */ 256 | protected function buildDictionary(Collection $results) 257 | { 258 | $foreign = $this->foreignKey; 259 | 260 | // First we will build a dictionary of child models keyed by the foreign key 261 | // of the relation so that we will easily and quickly match them to their 262 | // parents without having a possibly slow inner loops for every models. 263 | $dictionary = []; 264 | 265 | foreach ($results as $result) { 266 | foreach ($result->$foreign as $item) { 267 | $dictionary[$item][] = $result; 268 | } 269 | } 270 | 271 | return $dictionary; 272 | } 273 | 274 | /** 275 | * {@inheritdoc} 276 | */ 277 | protected function newPivotQuery() 278 | { 279 | return $this->newRelatedQuery(); 280 | } 281 | 282 | /** 283 | * Create a new query builder for the related model. 284 | * 285 | * @return \Illuminate\Database\Query\Builder 286 | */ 287 | public function newRelatedQuery() 288 | { 289 | return $this->related->newQuery(); 290 | } 291 | 292 | /** 293 | * Get the fully qualified foreign key for the relation. 294 | * 295 | * @return string 296 | */ 297 | public function getForeignKey() 298 | { 299 | return $this->foreignKey; 300 | } 301 | 302 | /** 303 | * {@inheritdoc} 304 | */ 305 | public function getQualifiedForeignKeyName() 306 | { 307 | return $this->foreignKey; 308 | } 309 | 310 | /** 311 | * Format the sync list so that it is keyed by ID. (Legacy Support) 312 | * The original function has been renamed to formatRecordsList since Laravel 5.3. 313 | * 314 | * @deprecated 315 | * 316 | * @param array $records 317 | * 318 | * @return array 319 | */ 320 | protected function formatSyncList(array $records) 321 | { 322 | $results = []; 323 | foreach ($records as $id => $attributes) { 324 | if (!is_array($attributes)) { 325 | list($id, $attributes) = [$attributes, []]; 326 | } 327 | $results[$id] = $attributes; 328 | } 329 | 330 | return $results; 331 | } 332 | 333 | /** 334 | * Get the related key with backwards compatible support. 335 | * 336 | * @return string 337 | */ 338 | public function getRelatedKey() 339 | { 340 | //var_dump($this); 341 | return property_exists($this, 'relatedPivotKey') ? $this->relatedPivotKey : $this->relatedKey; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/Relations/EmbedsMany.php: -------------------------------------------------------------------------------- 1 | setRelation($relation, $this->related->newCollection()); 19 | } 20 | 21 | return $models; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getResults() 28 | { 29 | return $this->toCollection($this->getEmbedded()); 30 | } 31 | 32 | /** 33 | * Save a new model and attach it to the parent model. 34 | * 35 | * @param Model $model 36 | * 37 | * @return Model|bool 38 | */ 39 | public function performInsert(Model $model) 40 | { 41 | // Generate a new key if needed. 42 | if ($model->getKeyName() == '_id' and !$model->getKey()) { 43 | $model->setAttribute('_id', uniqid()); 44 | } 45 | 46 | // For deeply nested documents, let the parent handle the changes. 47 | if ($this->isNested()) { 48 | $this->associate($model); 49 | return $this->parent->save() ? $model : false; 50 | } 51 | 52 | // Push the new model to the database. 53 | $result = $this->getBaseQuery()->push($this->localKey, $model->getAttributes(), true); 54 | 55 | $result = current($result); 56 | 57 | // Attach the model to its parent. 58 | if ($result['ok']) { 59 | $this->associate($model); 60 | } 61 | 62 | //Reload parent values 63 | $isUnguarded = $this->parent->isUnguarded(); 64 | 65 | $this->parent->unguard(); 66 | $this->parent->fill($this->parent->fresh()->getAttributes()); 67 | 68 | if (!$isUnguarded) { 69 | $this->parent->reguard(); 70 | } 71 | 72 | $this->parent->syncOriginal(); 73 | 74 | return $result ? $model : false; 75 | } 76 | 77 | /** 78 | * Save an existing model and attach it to the parent model. 79 | * 80 | * @param Model $model 81 | * 82 | * @return Model|bool 83 | */ 84 | public function performUpdate(Model $model) 85 | { 86 | // For deeply nested documents, let the parent handle the changes. 87 | if ($this->isNested()) { 88 | $this->associate($model); 89 | return $this->parent->save(); 90 | } 91 | 92 | // Get the correct foreign key value. 93 | $foreignKey = $this->getForeignKeyValue($model); 94 | $entries = $this->parent->getOriginal($this->localKey); 95 | 96 | //update array with the new data 97 | foreach ($entries as &$entry) { 98 | if ($entry['_id'] == $foreignKey) { 99 | $entry = $model->getAttributes(); 100 | } 101 | } 102 | 103 | 104 | $isUnguarded = $this->parent->isUnguarded(); 105 | $this->parent->unguard(); 106 | 107 | $result = $this->parent->update([$this->localKey=>$entries]); 108 | 109 | if (!$isUnguarded) { 110 | $this->parent->reguard(); 111 | } 112 | 113 | $this->parent->setRelation($this->localKey, $this->toCollection($entries)); 114 | 115 | return $result ? $model : false; 116 | } 117 | 118 | /** 119 | * Delete an existing model and detach it from the parent model. 120 | * 121 | * @param Model $model 122 | * 123 | * @return int 124 | */ 125 | public function performDelete(Model $model) 126 | { 127 | // For deeply nested documents, let the parent handle the changes. 128 | if ($this->isNested()) { 129 | $this->dissociate($model); 130 | 131 | return $this->parent->save(); 132 | } 133 | 134 | // Get the correct foreign key value. 135 | $foreignKey = $this->getForeignKeyValue($model); 136 | 137 | $entries = $this->parent->getOriginal($this->localKey); 138 | 139 | $result = $this->getBaseQuery()->pull($this->localKey, $model->getAttributes()); 140 | $result = current($result); 141 | 142 | if ($result['ok']) { 143 | $this->dissociate($model); 144 | } 145 | 146 | //update parent rev 147 | $this->parent->setAttribute($this->parent->getRevisionAttributeName(), $result['rev']); 148 | $this->parent->syncOriginal(); 149 | 150 | return $result; 151 | } 152 | 153 | /** 154 | * Associate the model instance to the given parent, without saving it to the database. 155 | * 156 | * @param Model $model 157 | * 158 | * @return Model 159 | */ 160 | public function associate(Model $model) 161 | { 162 | if (!$this->contains($model)) { 163 | return $this->associateNew($model); 164 | } else { 165 | return $this->associateExisting($model); 166 | } 167 | } 168 | 169 | /** 170 | * Dissociate the model instance from the given parent, without saving it to the database. 171 | * 172 | * @param mixed $ids 173 | * 174 | * @return int 175 | */ 176 | public function dissociate($ids = []) 177 | { 178 | $ids = $this->getIdsArrayFrom($ids); 179 | 180 | $records = $this->getEmbedded(); 181 | 182 | $primaryKey = $this->related->getKeyName(); 183 | 184 | // Remove the document from the parent model. 185 | foreach ($records as $i => $record) { 186 | if (in_array($record[$primaryKey], $ids)) { 187 | unset($records[$i]); 188 | } 189 | } 190 | 191 | $this->setEmbedded($records); 192 | 193 | // We return the total number of deletes for the operation. The developers 194 | // can then check this number as a boolean type value or get this total count 195 | // of records deleted for logging, etc. 196 | return count($ids); 197 | } 198 | 199 | /** 200 | * Destroy the embedded models for the given IDs. 201 | * 202 | * @param mixed $ids 203 | * 204 | * @return int 205 | */ 206 | public function destroy($ids = []) 207 | { 208 | $count = 0; 209 | 210 | $ids = $this->getIdsArrayFrom($ids); 211 | 212 | // Get all models matching the given ids. 213 | $models = $this->getResults()->only($ids); 214 | 215 | // Pull the documents from the database. 216 | foreach ($models as $model) { 217 | if ($model->delete()) { 218 | $count++; 219 | } 220 | } 221 | 222 | return $count; 223 | } 224 | 225 | /** 226 | * Delete all embedded models. 227 | * 228 | * @return int 229 | */ 230 | public function delete() 231 | { 232 | // Overwrite the local key with an empty array. 233 | $result = $this->query->update([$this->localKey => []]); 234 | 235 | if ($result) { 236 | $this->setEmbedded([]); 237 | } 238 | 239 | return $result; 240 | } 241 | 242 | /** 243 | * Destroy alias. 244 | * 245 | * @param mixed $ids 246 | * 247 | * @return int 248 | */ 249 | public function detach($ids = []) 250 | { 251 | return $this->destroy($ids); 252 | } 253 | 254 | /** 255 | * Save alias. 256 | * 257 | * @param Model $model 258 | * 259 | * @return Model 260 | */ 261 | public function attach(Model $model) 262 | { 263 | return $this->save($model); 264 | } 265 | 266 | /** 267 | * Associate a new model instance to the given parent, without saving it to the database. 268 | * 269 | * @param Model $model 270 | * 271 | * @return Model 272 | */ 273 | protected function associateNew($model) 274 | { 275 | // Create a new key if needed. 276 | if (!$model->getAttribute('_id')) { 277 | $model->setAttribute('_id', uniqid()); 278 | } 279 | 280 | $records = $this->getEmbedded(); 281 | $model->setConnection($this->parent->getConnectionName()); 282 | 283 | // Add the new model to the embedded documents. 284 | $records[] = $model->getAttributes(); 285 | 286 | return $this->setEmbedded($records); 287 | } 288 | 289 | /** 290 | * Associate an existing model instance to the given parent, without saving it to the database. 291 | * 292 | * @param Model $model 293 | * 294 | * @return Model 295 | */ 296 | protected function associateExisting($model) 297 | { 298 | // Get existing embedded documents. 299 | $records = $this->getEmbedded(); 300 | 301 | $primaryKey = $this->related->getKeyName(); 302 | 303 | $key = $model->getKey(); 304 | 305 | // Replace the document in the parent model. 306 | foreach ($records as &$record) { 307 | if ($record[$primaryKey] == $key) { 308 | $record = $model->getAttributes(); 309 | break; 310 | } 311 | } 312 | 313 | return $this->setEmbedded($records); 314 | } 315 | 316 | /** 317 | * Get a paginator for the "select" statement. 318 | * 319 | * @param int $perPage 320 | * 321 | * @return \Illuminate\Pagination\AbstractPaginator 322 | */ 323 | public function paginate($perPage = null) 324 | { 325 | $page = Paginator::resolveCurrentPage(); 326 | $perPage = $perPage ?: $this->related->getPerPage(); 327 | 328 | $results = $this->getEmbedded(); 329 | 330 | $total = count($results); 331 | 332 | $start = ($page - 1) * $perPage; 333 | $sliced = array_slice($results, $start, $perPage); 334 | 335 | return new LengthAwarePaginator($sliced, $total, $perPage, $page, [ 336 | 'path' => Paginator::resolveCurrentPath(), 337 | ]); 338 | } 339 | 340 | /** 341 | * {@inheritdoc} 342 | */ 343 | protected function getEmbedded() 344 | { 345 | return parent::getEmbedded() ?: []; 346 | } 347 | 348 | /** 349 | * {@inheritdoc} 350 | */ 351 | protected function setEmbedded($models) 352 | { 353 | if (!is_array($models)) { 354 | $models = [$models]; 355 | } 356 | 357 | return parent::setEmbedded(array_values($models)); 358 | } 359 | 360 | /** 361 | * {@inheritdoc} 362 | */ 363 | public function __call($method, $parameters) 364 | { 365 | if (method_exists(Collection::class, $method)) { 366 | return call_user_func_array([$this->getResults(), $method], $parameters); 367 | } 368 | 369 | return parent::__call($method, $parameters); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/Relations/EmbedsOne.php: -------------------------------------------------------------------------------- 1 | setRelation($relation, null); 16 | } 17 | 18 | return $models; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getResults() 25 | { 26 | $embedded = $this->getEmbedded(); 27 | $model = $this->toModel($this->getEmbedded()); 28 | 29 | if ($model) { 30 | $collection = $this->eagerLoadRelations([$model]); 31 | $model = current($collection); 32 | } 33 | 34 | return $model; 35 | } 36 | 37 | /** 38 | * Save a new model and attach it to the parent model. 39 | * 40 | * @param Model $model 41 | * 42 | * @return Model|bool 43 | */ 44 | public function performInsert(Model $model) 45 | { 46 | // Generate a new key if needed. 47 | if ($model->getKeyName() == '_id' and !$model->getKey()) { 48 | $model->setAttribute('_id', uniqid()); 49 | } 50 | 51 | // For deeply nested documents, let the parent handle the changes. 52 | if ($this->isNested()) { 53 | $this->associate($model); 54 | 55 | return $this->parent->save() ? $model : false; 56 | } 57 | 58 | $result = $this->getBaseQuery()->update([$this->localKey => $model->getAttributes()]); 59 | 60 | $result = current($result); 61 | //update parent rev 62 | $this->parent->setAttribute($this->parent->getRevisionAttributeName(), $result['rev']); 63 | $this->parent->syncOriginal(); 64 | 65 | // Attach the model to its parent. 66 | if ($result) { 67 | $this->associate($model); 68 | } 69 | 70 | return $result ? $model : false; 71 | } 72 | 73 | /** 74 | * Save an existing model and attach it to the parent model. 75 | * 76 | * @param Model $model 77 | * 78 | * @return Model|bool 79 | */ 80 | public function performUpdate(Model $model) 81 | { 82 | if ($this->isNested()) { 83 | $this->associate($model); 84 | 85 | return $this->parent->save(); 86 | } 87 | 88 | $result = $this->getBaseQuery()->update([$this->localKey => $model->getAttributes()]); 89 | 90 | $result = current($result); 91 | //update parent rev 92 | $this->parent->setAttribute($this->parent->getRevisionAttributeName(), $result['rev']); 93 | $this->parent->syncOriginal(); 94 | 95 | // Attach the model to its parent. 96 | if ($result) { 97 | $this->associate($model); 98 | } 99 | 100 | return $result ? $model : false; 101 | } 102 | 103 | /** 104 | * Delete an existing model and detach it from the parent model. 105 | * 106 | * @return int 107 | */ 108 | public function performDelete() 109 | { 110 | // For deeply nested documents, let the parent handle the changes. 111 | if ($this->isNested()) { 112 | $this->dissociate(); 113 | 114 | return $this->parent->save(); 115 | } 116 | 117 | // Overwrite the local key with an empty array. 118 | $result = $this->getBaseQuery()->update([$this->localKey => null]); 119 | 120 | // Detach the model from its parent. 121 | if ($result) { 122 | $this->dissociate(); 123 | } 124 | 125 | return $result; 126 | } 127 | 128 | /** 129 | * Attach the model to its parent. 130 | * 131 | * @param Model $model 132 | * 133 | * @return Model 134 | */ 135 | public function associate(Model $model) 136 | { 137 | return $this->setEmbedded($model->getAttributes()); 138 | } 139 | 140 | /** 141 | * Detach the model from its parent. 142 | * 143 | * @return Model 144 | */ 145 | public function dissociate() 146 | { 147 | return $this->setEmbedded(null); 148 | } 149 | 150 | /** 151 | * Delete all embedded models. 152 | * 153 | * @return int 154 | */ 155 | public function delete() 156 | { 157 | return $this->performDelete(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/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 | $this->addConstraints(); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function addConstraints() 59 | { 60 | if (static::$constraints) { 61 | $this->query->where($this->getQualifiedParentKeyName(), '=', $this->getParentKey()); 62 | //Force use of index 63 | $this->query->orderBy($this->getQualifiedParentKeyName()); 64 | } 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function addEagerConstraints(array $models) 71 | { 72 | // There is no eager loading constraint. 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function match(array $models, Collection $results = null, $relation) 79 | { 80 | /** 81 | * Basically $results will always be null since we're not querying them. 82 | * We're going to considere $results per model retriving them from the 83 | * very same model 84 | */ 85 | foreach ($models as $model) { 86 | //store the eager load 87 | $eagerLoad = $this->query->getEagerLoads(); 88 | $relationObject = $model->$relation(); 89 | 90 | //repass the eager load values to the relation query 91 | $relationObject->query->setEagerLoads($eagerLoad); 92 | 93 | $results = $relationObject->getResults(); 94 | $model->setParentRelation($this->parent, $this->relation); 95 | $model->setRelation($relation, $results); 96 | } 97 | 98 | return $models; 99 | } 100 | 101 | /** 102 | * This method is used to query the eager values from the database 103 | * and match them to all models. 104 | * Since we already have the data embedded, we won't query nothing 105 | * 106 | * {@inheritdoc} 107 | */ 108 | public function getEager(){ 109 | return null; 110 | } 111 | 112 | 113 | /** 114 | * Shorthand to get the results of the relationship. 115 | * 116 | * @return Collection 117 | */ 118 | public function get($colums = array()) 119 | { 120 | return $this->getResults($colums); 121 | } 122 | 123 | /** 124 | * Get the number of embedded models. 125 | * 126 | * @return int 127 | */ 128 | public function count() 129 | { 130 | return count($this->getEmbedded()); 131 | } 132 | 133 | /** 134 | * Attach a model instance to the parent model. 135 | * 136 | * @param Model $model 137 | * 138 | * @return Model|bool 139 | */ 140 | public function save(Model $model) 141 | { 142 | $model->setParentRelation($this->parent, $this->relation); 143 | 144 | return $model->save() ? $model : false; 145 | } 146 | 147 | /** 148 | * Attach a collection of models to the parent instance. 149 | * 150 | * @param Collection|array $models 151 | * 152 | * @return Collection|array 153 | */ 154 | public function saveMany($models) 155 | { 156 | foreach ($models as $model) { 157 | $this->save($model); 158 | } 159 | 160 | return $models; 161 | } 162 | 163 | /** 164 | * Create a new instance of the related model. 165 | * 166 | * @param array $attributes 167 | * 168 | * @return Model 169 | */ 170 | public function create(array $attributes) 171 | { 172 | // Here we will set the raw attributes to avoid hitting the "fill" method so 173 | // that we do not have to worry about a mass accessor rules blocking sets 174 | // on the models. Otherwise, some of these attributes will not get set. 175 | $instance = $this->related->newInstance($attributes); 176 | 177 | $instance->setParentRelation($this->parent, $this->relation); 178 | 179 | $instance->save(); 180 | 181 | return $instance; 182 | } 183 | 184 | /** 185 | * Create an array of new instances of the related model. 186 | * 187 | * @param array $records 188 | * 189 | * @return array 190 | */ 191 | public function createMany(array $records) 192 | { 193 | $instances = []; 194 | 195 | foreach ($records as $record) { 196 | $instances[] = $this->create($record); 197 | } 198 | 199 | return $instances; 200 | } 201 | 202 | /** 203 | * Transform single ID, single Model or array of Models into an array of IDs. 204 | * 205 | * @param mixed $ids 206 | * 207 | * @return array 208 | */ 209 | protected function getIdsArrayFrom($ids) 210 | { 211 | if ($ids instanceof \Illuminate\Support\Collection) { 212 | $ids = $ids->all(); 213 | } 214 | 215 | if (!is_array($ids)) { 216 | $ids = [$ids]; 217 | } 218 | 219 | foreach ($ids as &$id) { 220 | if ($id instanceof Model) { 221 | $id = $id->getKey(); 222 | } 223 | } 224 | 225 | return $ids; 226 | } 227 | 228 | /** 229 | * {@inheritdoc} 230 | */ 231 | protected function getEmbedded() 232 | { 233 | // Get raw attributes to skip relations and accessors. 234 | $attributes = $this->parent->getAttributes(); 235 | 236 | // Get embedded models form parent attributes. 237 | $embedded = isset($attributes[$this->localKey]) ? (array) $attributes[$this->localKey] : null; 238 | 239 | return $embedded; 240 | } 241 | 242 | /** 243 | * {@inheritdoc} 244 | */ 245 | protected function setEmbedded($records) 246 | { 247 | // Assign models to parent attributes array. 248 | $attributes = $this->parent->getAttributes(); 249 | $attributes[$this->localKey] = $records; 250 | 251 | // Set raw attributes to skip mutators. 252 | $this->parent->setRawAttributes($attributes); 253 | 254 | // Set the relation on the parent. 255 | return $this->parent->setRelation($this->relation, $records === null ? null : $this->getResults()); 256 | } 257 | 258 | /** 259 | * Get the foreign key value for the relation. 260 | * 261 | * @param mixed $id 262 | * 263 | * @return mixed 264 | */ 265 | protected function getForeignKeyValue($id) 266 | { 267 | if ($id instanceof Model) { 268 | $id = $id->getKey(); 269 | } 270 | 271 | return $id; 272 | } 273 | 274 | /** 275 | * Convert an array of records to a Collection. 276 | * 277 | * @param array $records 278 | * 279 | * @return Collection 280 | */ 281 | protected function toCollection(array $records = []) 282 | { 283 | $models = []; 284 | 285 | foreach ($records as $attributes) { 286 | $models[] = $this->toModel($attributes); 287 | } 288 | 289 | if (count($models) > 0) { 290 | $models = $this->eagerLoadRelations($models); 291 | } 292 | 293 | return new Collection($models); 294 | } 295 | 296 | /** 297 | * Create a related model instanced. 298 | * 299 | * @param array $attributes 300 | * 301 | * @return Model 302 | */ 303 | protected function toModel($attributes = []) 304 | { 305 | if (is_null($attributes)) { 306 | return; 307 | } 308 | 309 | $model = $this->related->newFromBuilder((array) $attributes); 310 | $model->setParentRelation($this->parent, $this->relation); 311 | $model->setRelation($this->foreignKey, $this->parent); 312 | $model->setConnection($this->parent->getConnectionName()); 313 | 314 | // If you remove this, you will get segmentation faults! 315 | $model->setHidden(array_merge($model->getHidden(), [$this->foreignKey])); 316 | 317 | return $model; 318 | } 319 | 320 | /** 321 | * Get the relation instance of the parent. 322 | * 323 | * @return Relation 324 | */ 325 | protected function getParentRelation() 326 | { 327 | return $this->parent->getParentRelation(); 328 | } 329 | 330 | /** 331 | * {@inheritdoc} 332 | */ 333 | public function getQuery() 334 | { 335 | // Because we are sharing this relation instance to models, we need 336 | // to make sure we use separate query instances. 337 | return $this->query;; 338 | } 339 | 340 | /** 341 | * {@inheritdoc} 342 | */ 343 | public function getBaseQuery() 344 | { 345 | // Because we are sharing this relation instance to models, we need 346 | // to make sure we use separate query instances. 347 | return clone $this->query->getQuery(); 348 | } 349 | 350 | /** 351 | * Check if this relation is nested in another relation. 352 | * 353 | * @return bool 354 | */ 355 | protected function isNested() 356 | { 357 | return $this->getParentRelation() != null; 358 | } 359 | 360 | /** 361 | * Get the fully qualified local key name. 362 | * 363 | * @param string $glue 364 | * 365 | * @return string 366 | */ 367 | protected function getPathHierarchy($glue = '.') 368 | { 369 | if ($parentRelation = $this->getParentRelation()) { 370 | return $parentRelation->getPathHierarchy($glue).$glue.$this->localKey; 371 | } 372 | 373 | return $this->localKey; 374 | } 375 | 376 | /** 377 | * {@inheritdoc} 378 | */ 379 | public function getQualifiedParentKeyName() 380 | { 381 | if ($parentRelation = $this->getParentRelation()) { 382 | return $parentRelation->getPathHierarchy().'.'.$this->parent->getKeyName(); 383 | } 384 | 385 | return $this->parent->getKeyName(); 386 | } 387 | 388 | /** 389 | * Get the primary key value of the parent. 390 | * 391 | * @return string 392 | */ 393 | protected function getParentKey() 394 | { 395 | return $this->parent->getKey(); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /src/Relations/HasMany.php: -------------------------------------------------------------------------------- 1 | foreignKey; 18 | } 19 | 20 | /** 21 | * Get the plain foreign key. 22 | * 23 | * @return string 24 | */ 25 | public function getPlainForeignKey() 26 | { 27 | return $this->getForeignKeyName(); 28 | } 29 | 30 | /** 31 | * Get the key for comparing against the parent key in "has" query. 32 | * 33 | * @return string 34 | */ 35 | public function getHasCompareKey() 36 | { 37 | return $this->getForeignKeyName(); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 44 | { 45 | $foreignKey = $this->getHasCompareKey(); 46 | 47 | return $query->select($foreignKey)->where($foreignKey, 'exists', true); 48 | } 49 | 50 | /** 51 | * Add the constraints for a relationship count query. 52 | * 53 | * @param Builder $query 54 | * @param Builder $parent 55 | * 56 | * @return Builder 57 | */ 58 | public function getRelationCountQuery(Builder $query, Builder $parent) 59 | { 60 | $foreignKey = $this->getHasCompareKey(); 61 | 62 | return $query->select($foreignKey)->where($foreignKey, 'exists', true); 63 | } 64 | 65 | /** 66 | * Add the constraints for a relationship query. 67 | * 68 | * @param Builder $query 69 | * @param Builder $parent 70 | * @param array|mixed $columns 71 | * 72 | * @return Builder 73 | */ 74 | public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) 75 | { 76 | $query->select($columns); 77 | 78 | $key = $this->wrap($this->getQualifiedParentKeyName()); 79 | 80 | return $query->where($this->getHasCompareKey(), 'exists', true); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Relations/HasOne.php: -------------------------------------------------------------------------------- 1 | foreignKey; 18 | } 19 | 20 | /** 21 | * Get the key for comparing against the parent key in "has" query. 22 | * 23 | * @return string 24 | */ 25 | public function getHasCompareKey() 26 | { 27 | return $this->getForeignKeyName(); 28 | } 29 | 30 | /** 31 | * Get the plain foreign key. 32 | * 33 | * @return string 34 | */ 35 | public function getPlainForeignKey() 36 | { 37 | return $this->getForeignKeyName(); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) 44 | { 45 | $foreignKey = $this->getForeignKeyName(); 46 | 47 | return $query->select($foreignKey)->where($foreignKey, 'exists', true); 48 | } 49 | 50 | /** 51 | * Add the constraints for a relationship count query. 52 | * 53 | * @param Builder $query 54 | * @param Builder $parent 55 | * 56 | * @return Builder 57 | */ 58 | public function getRelationCountQuery(Builder $query, Builder $parent) 59 | { 60 | $foreignKey = $this->getForeignKeyName(); 61 | 62 | return $query->select($foreignKey)->where($foreignKey, 'exists', true); 63 | } 64 | 65 | /** 66 | * Add the constraints for a relationship query. 67 | * 68 | * @param Builder $query 69 | * @param Builder $parent 70 | * @param array|mixed $columns 71 | * 72 | * @return Builder 73 | */ 74 | public function getRelationQuery(Builder $query, Builder $parent, $columns = ['*']) 75 | { 76 | $query->select($columns); 77 | 78 | $key = $this->wrap($this->getQualifiedParentKeyName()); 79 | 80 | return $query->where($this->getForeignKeyName(), 'exists', true); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Relations/MorphTo.php: -------------------------------------------------------------------------------- 1 | query->where($this->getOwnerKey(), '=', $this->parent->{$this->foreignKey}); 19 | //Force use of index 20 | $this->query->orderBy($this->getOwnerKey()); 21 | } 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function getResultsByType($type) 28 | { 29 | $instance = $this->createModelByType($type); 30 | 31 | $key = $instance->getKeyName(); 32 | 33 | $query = $instance->newQuery(); 34 | 35 | return $query->whereIn($key, $this->gatherKeysByType($type))->get(); 36 | } 37 | 38 | /** 39 | * Get the owner key with backwards compatible support. 40 | * 41 | * @return string 42 | */ 43 | public function getOwnerKey() 44 | { 45 | return property_exists($this, 'ownerKey') ? $this->ownerKey : $this->otherKey; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Relations/MorphToMany.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 27 | $this->fresh_query = clone $query; 28 | parent::__construct($query, $parent, $name, $table, $foreignKey, $relatedKey, $relationName, $inverse); 29 | } 30 | 31 | 32 | public function get($columns = ['*']) 33 | { 34 | 35 | //Retrive pivot entries 36 | $pivot = \DB::collection($this->table)->where([ 37 | [$this->morphType,'=',$this->morphClass], 38 | [$this->foreignKey,'=',$this->parent->id] 39 | ])->get(); 40 | 41 | $builder = $this->fresh_query->applyScopes(); 42 | $builder->whereIn('_id', $pivot->pluck($this->relatedKey)->toArray()); 43 | 44 | $models = $builder->getModels(); 45 | 46 | $this->hydratePivotRelation($models); 47 | 48 | // If we actually found models we will also eager load any relationships that 49 | // have been specified as needing to be eager loaded. This will solve the 50 | // n + 1 query problem for the developer and also increase performance. 51 | if (count($models) > 0) { 52 | $models = $builder->eagerLoadRelations($models); 53 | } 54 | 55 | return $this->related->newCollection($models); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Schema/Grammar.php: -------------------------------------------------------------------------------- 1 | app['db']); 18 | 19 | Model::setEventDispatcher($this->app['events']); 20 | } 21 | 22 | /** 23 | * Register the provider. 24 | * 25 | * @return void 26 | */ 27 | public function register() 28 | { 29 | // Add couchdb to the database manager 30 | $this->app->resolving('db', function ($db) { 31 | $db->extend('couchdb', function ($config, $name) { 32 | $config['name'] = $name; 33 | return new Connection($config); 34 | }); 35 | }); 36 | // Add connector for queue support. 37 | $this->app->resolving('queue', function ($queue) { 38 | $queue->addConnector('couchdb', function () { 39 | return new CouchConnector($this->app['db']); 40 | }); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/AuthTest.php: -------------------------------------------------------------------------------- 1 | 'John Doe', 18 | 'email' => 'john@doe.com', 19 | 'password' => Hash::make('foobar'), 20 | ]); 21 | 22 | $this->assertTrue(Auth::attempt(['email' => 'john@doe.com', 'password' => 'foobar'], true)); 23 | $this->assertTrue(Auth::check()); 24 | } 25 | 26 | protected function getMocks() 27 | { 28 | $mocks = [ 29 | 'tokens' => Mockery::mock('Illuminate\Auth\Passwords\TokenRepositoryInterface'), 30 | 'users' => Mockery::mock('Illuminate\Contracts\Auth\UserProvider'), 31 | 'mailer' => Mockery::mock('Illuminate\Contracts\Mail\Mailer'), 32 | 'view' => 'resetLinkView', 33 | ]; 34 | return $mocks; 35 | } 36 | 37 | public function testRemind() 38 | { 39 | $this->markTestSkipped('Not finished'); 40 | 41 | $mailer = Mockery::mock('Illuminate\Mail\Mailer'); 42 | $tokens = Password::getRepository(); 43 | $users = $this->app['auth']->getProvider(); 44 | 45 | $broker = new PasswordBroker($tokens, $users, $mailer, 'resetLinkView'); 46 | $user = User::create([ 47 | 'name' => 'John Doe', 48 | 'email' => 'john@doe.com', 49 | 'password' => Hash::make('foobar'), 50 | ]); 51 | 52 | $mailer->shouldReceive('send')->once(); 53 | $response = $broker->sendResetLink(['email'=>'john@doe.com']); 54 | $this->assertEquals(PasswordBroker::RESET_LINK_SENT, $response); 55 | 56 | $this->assertEquals(1, DB::collection('password_resets')->count()); 57 | $reminder = DB::collection('password_resets')->first(); 58 | $this->assertEquals('john@doe.com', $reminder['email']); 59 | $this->assertNotNull($reminder['token']); 60 | $this->assertTrue(is_string($reminder['created_at'])); 61 | $credentials = [ 62 | 'email' => 'john@doe.com', 63 | 'password' => 'foobar', 64 | 'password_confirmation' => 'foobar', 65 | 'token' => $reminder['token'], 66 | ]; 67 | 68 | $response = $broker->reset($credentials, function ($user, $password) { 69 | $user->password = bcrypt($password); 70 | $user->save(); 71 | }); 72 | 73 | $this->assertEquals('passwords.token', $response); 74 | 75 | $this->assertEquals(0, DB::collection('password_resets')->count()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/CollectionTest.php: -------------------------------------------------------------------------------- 1 | 'bar']; 11 | $where = ['id' => new ObjectID('56f94800911dcc276b5723dd')]; 12 | $time = 1.1; 13 | $queryString = 'name-collection.findOne({"id":"56f94800911dcc276b5723dd"})'; 14 | 15 | $mongoCollection = $this->getMockBuilder(MongoCollection::class) 16 | ->disableOriginalConstructor() 17 | ->getMock(); 18 | 19 | $mongoCollection->expects($this->once())->method('findOne')->with($where)->willReturn($return); 20 | $mongoCollection->expects($this->once())->method('getCollectionName')->willReturn('name-collection'); 21 | 22 | $connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock(); 23 | $connection->expects($this->once())->method('logging')->willReturn(true); 24 | $connection->expects($this->once())->method('getElapsedTime')->willReturn($time); 25 | $connection->expects($this->once())->method('logQuery')->with($queryString, [], $time); 26 | 27 | $collection = new Collection($connection, $mongoCollection); 28 | 29 | $this->assertEquals($return, $collection->findOne($where)); 30 | }*/ 31 | 32 | /** 33 | * @expectedException Exception 34 | */ 35 | public function testExecuteUnknownView() 36 | { 37 | $connection = DB::connection('couchdb'); 38 | 39 | $collection = new Collection($connection, 'unit-test-collection'); 40 | 41 | $this->assertInstanceOf('Robsonvn\CouchDB\Collection', $collection); 42 | $query = $collection->createViewQuery('all'); 43 | 44 | $this->assertInstanceOf('Doctrine\CouchDB\View\Query', $query); 45 | 46 | //Doctrine\CouchDB\HTTP\HTTPException 47 | $result = $query->execute(); 48 | } 49 | 50 | /*public function testExecuteView() 51 | { 52 | $connection = DB::connection('couchdb'); 53 | 54 | $collection = new Collection($connection, 'articles'); 55 | 56 | $this->assertInstanceOf('Robsonvn\CouchDB\Collection',$collection); 57 | $query = $collection->createViewQuery('all'); 58 | 59 | $this->assertInstanceOf('Doctrine\CouchDB\View\Query',$query); 60 | $query->setReduce(false); 61 | $query->setIncludeDocs(true); 62 | $result = $query->execute(); 63 | print_r($result); 64 | }*/ 65 | } 66 | -------------------------------------------------------------------------------- /tests/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Robsonvn\CouchDB\Connection', $connection); 9 | } 10 | 11 | public function testReconnect() 12 | { 13 | $c1 = DB::connection('couchdb'); 14 | $c2 = DB::connection('couchdb'); 15 | $this->assertEquals(spl_object_hash($c1), spl_object_hash($c2)); 16 | 17 | $c1 = DB::connection('couchdb'); 18 | DB::purge('couchdb'); 19 | $c2 = DB::connection('couchdb'); 20 | $this->assertNotEquals(spl_object_hash($c1), spl_object_hash($c2)); 21 | } 22 | 23 | public function testGetCouchDBClient() 24 | { 25 | $connection = DB::connection('couchdb'); 26 | $this->assertInstanceOf('Doctrine\CouchDB\CouchDBClient', $connection->getCouchDBClient()); 27 | } 28 | 29 | public function testCollection() 30 | { 31 | $collection = DB::connection('couchdb')->getCollection('unittest'); 32 | $this->assertInstanceOf('Robsonvn\CouchDB\Collection', $collection); 33 | /* 34 | $collection = DB::connection('couchdb')->collection('unittests'); 35 | $this->assertInstanceOf('Robsonvn\CouchDB\Query\Builder', $collection); 36 | 37 | $collection = DB::connection('couchdb')->table('unittests'); 38 | $this->assertInstanceOf('Robsonvn\CouchDB\Query\Builder', $collection);*/ 39 | } 40 | 41 | public function testDriverName() 42 | { 43 | $driver = DB::connection('couchdb')->getDriverName(); 44 | $this->assertEquals('couchdb', $driver); 45 | } 46 | 47 | /* 48 | // public function testDynamic() 49 | // { 50 | // $dbs = DB::connection('couchdb')->listCollections(); 51 | // $this->assertTrue(is_array($dbs)); 52 | // } 53 | 54 | // public function testMultipleConnections() 55 | // { 56 | // global $app; 57 | 58 | // # Add fake host 59 | // $db = $app['config']['database.connections']['mongodb']; 60 | // $db['host'] = array($db['host'], '1.2.3.4'); 61 | 62 | // $connection = new Connection($db); 63 | // $mongoclient = $connection->getMongoClient(); 64 | 65 | // $hosts = $mongoclient->getHosts(); 66 | // $this->assertEquals(1, count($hosts)); 67 | // } 68 | 69 | public function testQueryLog() 70 | { 71 | DB::enableQueryLog(); 72 | 73 | $this->assertEquals(0, count(DB::getQueryLog())); 74 | 75 | DB::collection('items')->get(); 76 | $this->assertEquals(1, count(DB::getQueryLog())); 77 | 78 | DB::collection('items')->insert(['name' => 'test']); 79 | $this->assertEquals(2, count(DB::getQueryLog())); 80 | 81 | DB::collection('items')->count(); 82 | $this->assertEquals(3, count(DB::getQueryLog())); 83 | 84 | DB::collection('items')->where('name', 'test')->update(['name' => 'test']); 85 | $this->assertEquals(4, count(DB::getQueryLog())); 86 | 87 | DB::collection('items')->where('name', 'test')->delete(); 88 | $this->assertEquals(5, count(DB::getQueryLog())); 89 | } 90 | 91 | public function testSchemaBuilder() 92 | { 93 | $schema = DB::connection('couchdb')->getSchemaBuilder(); 94 | $this->assertInstanceOf('Robsonvn\CouchDB\Schema\Builder', $schema); 95 | } 96 | 97 | public function testDriverName() 98 | { 99 | $driver = DB::connection('couchdb')->getDriverName(); 100 | $this->assertEquals('mongodb', $driver); 101 | } 102 | 103 | 104 | 105 | public function testCustomHostAndPort() 106 | { 107 | Config::set('database.connections.mongodb.host', 'db1'); 108 | Config::set('database.connections.mongodb.port', 27000); 109 | 110 | $connection = DB::connection('couchdb'); 111 | $this->assertEquals("mongodb://db1:27000", (string) $connection->getMongoClient()); 112 | } 113 | 114 | public function testHostWithPorts() 115 | { 116 | Config::set('database.connections.mongodb.port', 27000); 117 | Config::set('database.connections.mongodb.host', ['db1:27001', 'db2:27002', 'db3:27000']); 118 | 119 | $connection = DB::connection('couchdb'); 120 | $this->assertEquals('mongodb://db1:27001,db2:27002,db3:27000', (string) $connection->getMongoClient()); 121 | }*/ 122 | } 123 | -------------------------------------------------------------------------------- /tests/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/ModelTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Model::class, $user); 22 | $this->assertInstanceOf('Robsonvn\CouchDB\Connection', $user->getConnection()); 23 | $this->assertEquals(false, $user->exists); 24 | $this->assertEquals('users', $user->getTable()); 25 | $this->assertEquals('_id', $user->getKeyName()); 26 | } 27 | 28 | public function testInsert() 29 | { 30 | $user = new User(); 31 | $user->name = 'John Doe'; 32 | $user->title = 'admin'; 33 | $user->birthday = new DateTime('1980/1/1'); 34 | $user->age = 35; 35 | 36 | $user->save(); 37 | 38 | $this->assertEquals(true, $user->exists); 39 | $this->assertEquals(1, User::count()); 40 | 41 | $this->assertTrue(isset($user->_id)); 42 | 43 | $this->assertTrue(is_string($user->_id)); 44 | $this->assertNotEquals('', (string) $user->_id); 45 | $this->assertNotEquals(0, strlen((string) $user->_id)); 46 | $this->assertInstanceOf(Carbon::class, $user->created_at); 47 | 48 | $this->assertEquals('John Doe', $user->name); 49 | $this->assertEquals(35, $user->age); 50 | } 51 | 52 | public function testAll() 53 | { 54 | $user = new User(); 55 | $user->name = 'John Doe'; 56 | $user->title = 'admin'; 57 | $user->age = 35; 58 | $user->save(); 59 | 60 | $user = new User(); 61 | $user->name = 'Jane Doe'; 62 | $user->title = 'user'; 63 | $user->age = 32; 64 | $user->save(); 65 | 66 | $all = User::all(); 67 | 68 | $this->assertEquals(2, count($all)); 69 | 70 | $this->assertContains('John Doe', $all->pluck('name')); 71 | $this->assertContains('Jane Doe', $all->pluck('name')); 72 | } 73 | 74 | public function testFind() 75 | { 76 | $user = new User(); 77 | $user->name = 'John Doe'; 78 | $user->title = 'admin'; 79 | $user->age = 35; 80 | $user->save(); 81 | 82 | $check = User::find($user->id); 83 | 84 | $this->assertInstanceOf(User::class, $check); 85 | $this->assertEquals($user->name, $check->name); 86 | $this->assertEquals($user->title, $check->title); 87 | $this->assertEquals($user->age, $check->age); 88 | $this->assertEquals($user->updated_at, $check->updated_at); 89 | $this->assertEquals($user->created_at, $check->created_at); 90 | } 91 | 92 | public function testInsertWithId() 93 | { 94 | $user = new User(); 95 | $user->id = '1'; 96 | $user->name = 'John Doe'; 97 | $user->title = 'admin'; 98 | $user->age = 35; 99 | 100 | $user->save(); 101 | 102 | $this->assertEquals(true, $user->exists); 103 | $this->assertEquals(1, User::count()); 104 | 105 | $check = User::find($user->id); 106 | $this->assertInstanceOf(User::class, $check); 107 | $this->assertEquals($user->name, $check->name); 108 | $this->assertEquals($user->title, $check->title); 109 | $this->assertEquals($user->age, $check->age); 110 | $this->assertEquals($user->created_at, $check->created_at); 111 | $this->assertEquals($user->updated_at, $check->updated_at); 112 | } 113 | 114 | public function testUpdate() 115 | { 116 | $user = new User(); 117 | $user->name = 'John Doe'; 118 | $user->title = 'admin'; 119 | $user->age = 35; 120 | $user->save(); 121 | 122 | //Sleep one second to confront updated_at 123 | sleep(1); 124 | 125 | $last_updated_at = $user->updated_at; 126 | $last_rev = $user->_rev; 127 | $user->age = 36; 128 | 129 | $user->save(); 130 | 131 | $this->assertNotEquals($last_rev, $user->_rev); 132 | $this->assertNotEquals($last_updated_at, $user->updated_at); 133 | $this->assertEquals(36, $user->age); 134 | 135 | $check = User::find($user->_id); 136 | 137 | $this->assertEquals(true, $check->exists); 138 | $this->assertInstanceOf(Carbon::class, $check->created_at); 139 | $this->assertInstanceOf(Carbon::class, $check->updated_at); 140 | $this->assertEquals(1, User::count()); 141 | 142 | $this->assertEquals($user->name, $check->name); 143 | $this->assertEquals($user->age, $check->age); 144 | $this->assertEquals($user->_rev, $check->_rev); 145 | 146 | $last_updated_at = $check->updated_at; 147 | $last_rev = $check->_rev; 148 | 149 | //Sleep one second to confront updated_at 150 | sleep(1); 151 | $user->update(['age' => 20]); 152 | 153 | $check = User::find($user->_id); 154 | $this->assertNotEquals($last_rev, $check->_rev); 155 | $this->assertNotEquals($last_updated_at, $check->updated_at); 156 | $this->assertEquals(20, $check->age); 157 | } 158 | 159 | public function testMultipleUpdate() 160 | { 161 | User::insert([ 162 | [ 163 | 'name' => 'John Doe', 164 | 'title' => 'admin', 165 | 'age' => 35 166 | ], 167 | [ 168 | 'name' => 'Jane Doe', 169 | 'title' => 'admin', 170 | 'age' => 35 171 | ], 172 | [ 173 | 'name' => 'John Doe Jr', 174 | 'title' => 'admin', 175 | 'age' => 10 176 | ] 177 | ]); 178 | $test = User::where('age', '=', 10)->update(['title'=>'user']); 179 | 180 | $this->assertEquals(1, User::where('title', 'user')->count()); 181 | } 182 | 183 | public function testUpdateIndexed() 184 | { 185 | $user = new User; 186 | $user->name = 'Jane Doe'; 187 | $user->title = 'admin'; 188 | $user->age = 35; 189 | $user->save(); 190 | 191 | $user->age = 23; 192 | $user->save(); 193 | } 194 | 195 | public function testSelect() 196 | { 197 | $user = new User(); 198 | $user->name = 'John Doe'; 199 | $user->title = 'admin'; 200 | $user->age = 35; 201 | $user->save(); 202 | 203 | $user = new User(); 204 | $user->name = 'Jane Doe'; 205 | $user->title = 'admin'; 206 | $user->age = 35; 207 | $user->save(); 208 | 209 | //Simple select 210 | $result = User::where('_id', $user->id)->first(); 211 | $this->assertInstanceOf(User::class, $result); 212 | 213 | $result = User::where('name', 'John Doe')->first(); 214 | $this->assertInstanceOf(User::class, $result); 215 | 216 | $result = User::where('title', 'admin')->get(); 217 | $this->assertEquals(2, $result->count()); 218 | 219 | //Nested where 220 | $result = User::where([['title', '=', 'admin']])->get(); 221 | $this->assertEquals(2, $result->count()); 222 | 223 | $result = User::where([['title', '=', 'admin'], ['name', '=', 'John Doe']])->get(); 224 | $this->assertEquals(1, $result->count()); 225 | } 226 | 227 | public function testDelete() 228 | { 229 | $user = new User(); 230 | $user->name = 'John Doe'; 231 | $user->title = 'admin'; 232 | $user->age = 35; 233 | $user->save(); 234 | 235 | $this->assertEquals(true, $user->exists); 236 | $this->assertEquals(1, User::count()); 237 | 238 | $user->delete(); 239 | 240 | $this->assertEquals(0, User::count()); 241 | } 242 | 243 | public function testGet() 244 | { 245 | $result = User::insert([ 246 | ['name' => 'John Doe'], 247 | ['name' => 'Jane Doe'], 248 | ]); 249 | 250 | $users = User::get(); 251 | 252 | $this->assertEquals(2, count($users)); 253 | $this->assertInstanceOf(Collection::class, $users); 254 | $this->assertInstanceOf(Model::class, $users[0]); 255 | } 256 | 257 | public function testFirst() 258 | { 259 | User::insert([ 260 | ['name' => 'John Doe'], 261 | ['name' => 'Jane Doe'], 262 | ]); 263 | 264 | $user = User::first(); 265 | 266 | $this->assertInstanceOf(Model::class, $user); 267 | $this->assertEquals('John Doe', $user->name); 268 | } 269 | 270 | public function testNoDocument() 271 | { 272 | $items = Item::where('name', 'nothing')->get(); 273 | $this->assertInstanceOf(Collection::class, $items); 274 | $this->assertEquals(0, $items->count()); 275 | 276 | $item = Item::where('name', 'nothing')->first(); 277 | $this->assertEquals(null, $item); 278 | 279 | $item = Item::find('51c33d8981fec6813e00000a'); 280 | $this->assertEquals(null, $item); 281 | } 282 | 283 | public function testFindOrfail() 284 | { 285 | $this->expectException(Illuminate\Database\Eloquent\ModelNotFoundException::class); 286 | User::findOrfail('51c33d8981fec6813e00000a'); 287 | } 288 | 289 | public function testCreate() 290 | { 291 | $user = User::create(['name' => 'Jane Poe']); 292 | 293 | $this->assertInstanceOf(Model::class, $user); 294 | $this->assertEquals(true, $user->exists); 295 | $this->assertEquals('Jane Poe', $user->name); 296 | $this->assertEquals(true, is_string($user->_rev)); 297 | 298 | $check = User::where('name', 'Jane Poe')->first(); 299 | $this->assertEquals($user->_id, $check->_id); 300 | } 301 | 302 | public function testDestroy() 303 | { 304 | $user = new User(); 305 | $user->name = 'John Doe'; 306 | $user->title = 'admin'; 307 | $user->age = 35; 308 | $user->save(); 309 | 310 | User::destroy((string) $user->_id); 311 | 312 | $this->assertEquals(0, User::count()); 313 | } 314 | 315 | public function testTouch() 316 | { 317 | $user = new User(); 318 | $user->name = 'John Doe'; 319 | $user->title = 'admin'; 320 | $user->age = 35; 321 | $user->save(); 322 | 323 | $old = $user->updated_at; 324 | 325 | sleep(1); 326 | $user->touch(); 327 | $check = User::find($user->_id); 328 | 329 | $this->assertNotEquals($old, $check->updated_at); 330 | } 331 | 332 | public function testSoftDelete() 333 | { 334 | Soft::create(['name' => 'John Doe']); 335 | $test = Soft::create(['name' => 'Jane Doe']); 336 | 337 | $all = Soft::all(); 338 | 339 | $this->assertEquals(2, Soft::count()); 340 | 341 | $user = Soft::where('name', 'John Doe')->first(); 342 | 343 | $this->assertEquals(true, $user->exists); 344 | $this->assertEquals(false, $user->trashed()); 345 | $this->assertNull($user->deleted_at); 346 | 347 | $user->delete(); 348 | 349 | $this->assertEquals(true, $user->trashed()); 350 | $this->assertNotNull($user->deleted_at); 351 | 352 | $user = Soft::where('name', 'John Doe')->first(); 353 | $this->assertNull($user); 354 | 355 | $this->assertEquals(1, Soft::count()); 356 | $this->assertEquals(2, Soft::withTrashed()->count()); 357 | 358 | $user = Soft::withTrashed()->where('name', 'John Doe')->first(); 359 | 360 | $this->assertNotNull($user); 361 | $this->assertInstanceOf(Carbon::class, $user->deleted_at); 362 | $this->assertEquals(true, $user->trashed()); 363 | 364 | $user->restore(); 365 | $all = Soft::withTrashed()->get(); 366 | 367 | $this->assertEquals(2, Soft::count()); 368 | } 369 | 370 | public function testScope() 371 | { 372 | Item::insert([ 373 | ['name' => 'knife', 'object_type' => 'sharp'], 374 | ['name' => 'spoon', 'object_type' => 'round'], 375 | ]); 376 | 377 | $sharp = Item::sharp()->get(); 378 | $this->assertEquals(1, $sharp->count()); 379 | } 380 | 381 | public function testGetMangoQuery() 382 | { 383 | $mangoQuery = User::where('age', '>', 37)->orderBy('age')->getMangoQuery(); 384 | $expected = 385 | [ 386 | '$and' => [ 387 | ['age'=> ['$gt'=>37,'$lt'=>'a']], 388 | ['type'=> 'users'] 389 | ] 390 | ]; 391 | $this->assertEquals($expected, $mangoQuery->selector()); 392 | $this->assertEquals(PHP_INT_MAX, $mangoQuery->limit()); 393 | $this->assertEquals([['type'=>'asc'],['age'=>'asc']], $mangoQuery->sort()); 394 | $this->assertEquals(['_design/mango-indexes','type:asc&age:asc'], $mangoQuery->use_index()); 395 | } 396 | 397 | public function testToArray() 398 | { 399 | $item = Item::create(['name' => 'fork', 'object_type' => 'sharp']); 400 | 401 | $array = $item->toArray(); 402 | 403 | $keys = array_keys($array); 404 | sort($keys); 405 | 406 | $this->assertEquals(['_id', '_rev', 'created_at', 'name', 'object_type', 'updated_at'], $keys); 407 | $this->assertTrue(is_string($array['created_at'])); 408 | $this->assertTrue(is_string($array['updated_at'])); 409 | $this->assertTrue(is_string($array['_id'])); 410 | $this->assertTrue(is_string($array['_rev'])); 411 | } 412 | 413 | public function testUnset() 414 | { 415 | $user1 = User::create(['name' => 'John Doe', 'note1' => 'ABC', 'note2' => 'DEF']); 416 | $user2 = User::create(['name' => 'Jane Doe', 'note1' => 'ABC', 'note2' => 'DEF']); 417 | 418 | $user1->unset('note1'); 419 | 420 | $this->assertFalse(isset($user1->note1)); 421 | $this->assertTrue(isset($user1->note2)); 422 | $this->assertTrue(isset($user2->note1)); 423 | $this->assertTrue(isset($user2->note2)); 424 | 425 | // Re-fetch to be sure 426 | $user1 = User::find($user1->_id); 427 | $user2 = User::find($user2->_id); 428 | 429 | $this->assertFalse(isset($user1->note1)); 430 | $this->assertTrue(isset($user1->note2)); 431 | $this->assertTrue(isset($user2->note1)); 432 | $this->assertTrue(isset($user2->note2)); 433 | 434 | $user2->unset(['note1', 'note2']); 435 | 436 | $this->assertFalse(isset($user2->note1)); 437 | $this->assertFalse(isset($user2->note2)); 438 | } 439 | 440 | public function testDates() 441 | { 442 | $birthday = new DateTime('1980/1/1'); 443 | $user = User::create(['name' => 'John Doe', 'birthday' => $birthday]); 444 | $this->assertInstanceOf(Carbon::class, $user->birthday); 445 | 446 | $check = User::find($user->_id); 447 | $this->assertInstanceOf(Carbon::class, $check->birthday); 448 | $this->assertEquals($user->birthday, $check->birthday); 449 | 450 | $user = User::where('birthday', '>', new DateTime('1975/1/1'))->first(); 451 | 452 | $this->assertEquals('John Doe', $user->name); 453 | 454 | // test custom date format for json output 455 | $json = $user->toArray(); 456 | $this->assertEquals($user->birthday->format('l jS \of F Y h:i:s A'), $json['birthday']); 457 | $this->assertEquals($user->created_at->format('l jS \of F Y h:i:s A'), $json['created_at']); 458 | 459 | // test created_at 460 | $item = Item::create(['name' => 'sword']); 461 | $this->assertRegExp('/^(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/', $item->getOriginal('created_at')); 462 | $this->assertEquals(strtotime($item->getOriginal('created_at')), $item->created_at->getTimestamp()); 463 | $this->assertTrue(abs(time() - $item->created_at->getTimestamp()) < 2); 464 | 465 | // test default date format for json output 466 | $item = Item::create(['name' => 'sword']); 467 | $json = $item->toArray(); 468 | $this->assertEquals($item->created_at->format('Y-m-d H:i:s'), $json['created_at']); 469 | 470 | $user = User::create(['name' => 'Jane Doe', 'birthday' => time()]); 471 | $this->assertInstanceOf(Carbon::class, $user->birthday); 472 | 473 | $user = User::create(['name' => 'Jane Doe', 'birthday' => 'Monday 8th of August 2005 03:12:46 PM']); 474 | $this->assertInstanceOf(Carbon::class, $user->birthday); 475 | 476 | $user = User::create(['name' => 'Jane Doe', 'birthday' => '2005-08-08']); 477 | $this->assertInstanceOf(Carbon::class, $user->birthday); 478 | /* 479 | $params = [ 480 | 'name' => 'ExtremeInsaneTest', 481 | 'entry' => [ 482 | 'date' => 'Monday 8th of August 2005 03:12:46 PM', 483 | 'logs'=>[ 484 | [ 485 | 'log_date'=>'Monday 8th of August 2005 03:12:46 PM', 486 | 'not_castable_data'=> 'Monday 9th of August 2005 03:12:46 PM', 487 | 'insane_tests'=>[ 488 | [ 489 | 'date'=>'Monday 8th of August 2005 03:12:46 PM' 490 | ], 491 | [ 492 | 'date'=>'Monday 8th of August 2005 03:12:46 PM' 493 | ] 494 | ] 495 | ] 496 | ], 497 | ], 498 | 'entry.extreme_insane_test' => [ 499 | 'dates'=>[ 500 | [ 501 | 'danger_date'=>'Monday 8th of August 2005 03:12:46 PM', 502 | ], 503 | [ 504 | 'danger_date'=>'Monday 8th of August 2005 03:12:46 PM', 505 | ], 506 | ] 507 | ] 508 | ]; 509 | 510 | $user = User::create($params); 511 | $this->assertInstanceOf(Carbon::class, $user->getAttribute('entry.date')); 512 | $this->assertInternalType('array',$logs = $user->getAttribute('entry.logs')); 513 | */ 514 | 515 | /*foreach($logs as $log){ 516 | $this->assertInstanceOf(Carbon::class, $log['log_date']); 517 | $this->assertNotInstanceOf(Carbon::class, $log['not_castable_data']); 518 | $this->assertInternalType('array', $log['insane_tests']); 519 | foreach($log['insane_tests'] as $insane){ 520 | $this->assertInstanceOf(Carbon::class, $insane['date']); 521 | echo $insane['date']; 522 | } 523 | }*/ 524 | 525 | //print_r($user); 526 | 527 | //return; 528 | 529 | $user = User::create(['name' => 'Jane Doe', 'entry' => ['date' => '2005-08-08']]); 530 | $this->assertInstanceOf(Carbon::class, $user->getAttribute('entry.date')); 531 | 532 | $user->setAttribute('entry.date', new DateTime()); 533 | $this->assertInstanceOf(Carbon::class, $user->getAttribute('entry.date')); 534 | 535 | $data = $user->toArray(); 536 | 537 | $this->assertEquals((string) $user->getAttribute('entry.date')->format('Y-m-d H:i:s'), $data['entry']['date']); 538 | } 539 | 540 | public function testPushPull() 541 | { 542 | $user = User::create(['name' => 'John Doe']); 543 | $last_rev = $user->_rev; 544 | //Simple 545 | $user->push('tags', 'tag1'); 546 | 547 | //verify new revision 548 | $this->assertEquals(true, is_string($user->_rev)); 549 | $this->assertNotEquals($last_rev, $user->_rev); 550 | $this->assertEquals(['tag1'], $user->tags); 551 | //fetch and check 552 | $user = User::where('_id', $user->_id)->first(); 553 | $this->assertEquals(['tag1'], $user->tags); 554 | 555 | //Simple array 556 | $user->push('tags', ['tag1', 'tag2']); 557 | $this->assertEquals(['tag1', 'tag1', 'tag2'], $user->tags); 558 | //fetch and check 559 | $user = User::where('_id', $user->_id)->first(); 560 | $this->assertEquals(['tag1', 'tag1', 'tag2'], $user->tags); 561 | 562 | //simple unique 563 | $user->push('tags', 'tag2', true); 564 | $this->assertEquals(['tag1', 'tag1', 'tag2'], $user->tags); 565 | //fetch and check 566 | $user = User::where('_id', $user->_id)->first(); 567 | $this->assertEquals(['tag1', 'tag1', 'tag2'], $user->tags); 568 | 569 | //simple pull 570 | $last_rev = $user->_rev; 571 | $user->pull('tags', 'tag1'); 572 | $this->assertNotEquals($last_rev, $user->_rev); 573 | $this->assertEquals(['tag2'], $user->tags); 574 | //fetch and check 575 | $user = User::where('_id', $user->_id)->first(); 576 | $this->assertEquals(['tag2'], $user->tags); 577 | 578 | //simple push again 579 | $user->push('tags', 'tag3'); 580 | $this->assertEquals(['tag2', 'tag3'], $user->tags); 581 | 582 | //remove all 583 | $user->pull('tags', ['tag2', 'tag3']); 584 | $this->assertEquals([], $user->tags); 585 | 586 | $user = User::where('_id', $user->_id)->first(); 587 | 588 | $this->assertEquals([], $user->tags); 589 | } 590 | 591 | public function testRaw() 592 | { 593 | User::create(['name' => 'John Doe', 'age' => 35]); 594 | User::create(['name' => 'Jane Doe', 'age' => 35]); 595 | User::create(['name' => 'Harry Hoe', 'age' => 15]); 596 | 597 | $users = User::raw(function ($collection) { 598 | return $collection->find(new MangoQuery(['age' => 35])); 599 | }); 600 | 601 | $this->assertInstanceOf(Model::class, $users[0]); 602 | 603 | $user = User::raw(function ($collection) { 604 | return $collection->findOne(['age' => 35]); 605 | }); 606 | 607 | $this->assertInstanceOf(Model::class, $user); 608 | 609 | $result = User::raw(function ($collection) { 610 | return $collection->insertOne(['name' => 'Yvonne Yoe', 'age' => 35]); 611 | }); 612 | $this->assertNotNull($result); 613 | } 614 | 615 | public function testDotNotation() 616 | { 617 | $user = User::create([ 618 | 'name' => 'John Doe', 619 | 'address' => [ 620 | 'city' => 'Paris', 621 | 'country' => 'France', 622 | ], 623 | ]); 624 | 625 | $this->assertEquals('Paris', $user->getAttribute('address.city')); 626 | $this->assertEquals('Paris', $user['address.city']); 627 | $this->assertEquals('Paris', $user->{'address.city'}); 628 | 629 | // Fill 630 | $user->fill([ 631 | 'address.city' => 'Strasbourg', 632 | ]); 633 | 634 | $this->assertEquals('Strasbourg', $user['address.city']); 635 | } 636 | 637 | public function testMultipleLevelDotNotation() 638 | { 639 | $book = Book::create([ 640 | 'title' => 'A Game of Thrones', 641 | 'chapters' => [ 642 | 'one' => [ 643 | 'title' => 'The first chapter', 644 | ], 645 | ], 646 | ]); 647 | 648 | $this->assertEquals(['one' => ['title' => 'The first chapter']], $book->chapters); 649 | $this->assertEquals(['title' => 'The first chapter'], $book['chapters.one']); 650 | $this->assertEquals('The first chapter', $book['chapters.one.title']); 651 | } 652 | 653 | public function testGetDirtyDates() 654 | { 655 | $this->markTestSkipped('i have to study it more, not implemented yet'); 656 | 657 | $user = new User(); 658 | $user->setRawAttributes(['name' => 'John Doe', 'birthday' => new DateTime('19 august 1989')], true); 659 | $this->assertEmpty($user->getDirty()); 660 | 661 | $user->birthday = new DateTime('19 august 1989'); 662 | $this->assertEmpty($user->getDirty()); 663 | } 664 | } 665 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | 'John Doe', 'age' => 35, 'title' => 'admin']); 11 | User::create(['name' => 'Jane Doe', 'age' => 33, 'title' => 'admin']); 12 | User::create(['name' => 'Harry Hoe', 'age' => 13, 'title' => 'user']); 13 | User::create(['name' => 'Robert Roe', 'age' => 37, 'title' => 'user']); 14 | User::create(['name' => 'Mark Moe', 'age' => 23, 'title' => 'user']); 15 | User::create(['name' => 'Brett Boe', 'age' => 35, 'title' => 'user']); 16 | User::create(['name' => 'Tommy Toe', 'age' => 33, 'title' => 'user']); 17 | User::create(['name' => 'Yvonne Yoe', 'age' => 35, 'title' => 'admin', 'privilleged' => true]); 18 | User::create(['name' => 'Error', 'age' => null, 'title' => null]); 19 | } 20 | 21 | public function tearDown() 22 | { 23 | User::truncate(); 24 | parent::tearDown(); 25 | } 26 | 27 | public function testWhere() 28 | { 29 | $users = User::where('age', 35)->get(); 30 | 31 | $this->assertEquals(3, count($users)); 32 | $users = User::where('age', '=', 35)->get(); 33 | $this->assertEquals(3, count($users)); 34 | 35 | $users = User::where('age', '>=', 35)->get(); 36 | 37 | $this->assertEquals(4, count($users)); 38 | 39 | $users = User::where('age', '<=', 18)->get(); 40 | 41 | $this->assertEquals(1, count($users)); 42 | 43 | $users = User::where('age', '!=', 35)->get(); 44 | $this->assertEquals(6, count($users)); 45 | 46 | $users = User::where('age', '<>', 35)->get(); 47 | $this->assertEquals(6, count($users)); 48 | } 49 | 50 | public function testAndWhere() 51 | { 52 | $users = User::where('age', 35)->where('title', 'admin')->get(); 53 | 54 | $this->assertEquals(2, count($users)); 55 | 56 | $users = User::where('age', '>=', 35)->where('title', 'user')->get(); 57 | $this->assertEquals(2, count($users)); 58 | } 59 | 60 | public function testRegex() 61 | { 62 | $users = User::where('name', 'regex', '(?i).*doe$')->get(); 63 | $this->assertEquals(2, count($users)); 64 | } 65 | 66 | public function testNotRegex() 67 | { 68 | $users = User::where('name', 'not regex', '(?i).*doe$')->get(); 69 | $this->assertEquals(7, count($users)); 70 | } 71 | 72 | public function testLike() 73 | { 74 | $users = User::where('name', 'like', '%Doe')->get(); 75 | $this->assertEquals(2, count($users)); 76 | 77 | $users = User::where('name', 'like', '%y%')->get(); 78 | $this->assertEquals(2, count($users)); 79 | 80 | $users = User::where('name', 'LIKE', '%y%')->get(); 81 | $this->assertEquals(2, count($users)); 82 | 83 | $users = User::where('name', 'like', 't%')->get(); 84 | $this->assertEquals(0, count($users)); 85 | } 86 | 87 | public function testILike() 88 | { 89 | $users = User::where('name', 'ilike', '%doe')->get(); 90 | $this->assertEquals(2, count($users)); 91 | 92 | $users = User::where('name', 'ilike', '%y%')->get(); 93 | $this->assertEquals(3, count($users)); 94 | 95 | $users = User::where('name', 'ILIKE', '%y%')->get(); 96 | $this->assertEquals(3, count($users)); 97 | 98 | $users = User::where('name', 'ilike', 't%')->get(); 99 | $this->assertEquals(1, count($users)); 100 | } 101 | 102 | public function testNotLike() 103 | { 104 | $users = User::where('name', 'not like', '%Doe')->get(); 105 | $this->assertEquals(7, count($users)); 106 | 107 | $users = User::where('name', 'not like', '%y%')->get(); 108 | $this->assertEquals(7, count($users)); 109 | 110 | $users = User::where('name', 'NOT LIKE', '%y%')->get(); 111 | $this->assertEquals(7, count($users)); 112 | 113 | $users = User::where('name', 'NOT like', 't%')->get(); 114 | $this->assertEquals(9, count($users)); 115 | } 116 | 117 | public function testiNotLike() 118 | { 119 | $users = User::where('name', 'not ilike', '%doe')->get(); 120 | $this->assertEquals(7, count($users)); 121 | 122 | $users = User::where('name', 'not ilike', '%y%')->get(); 123 | $this->assertEquals(6, count($users)); 124 | 125 | $users = User::where('name', 'NOT ILIKE', '%y%')->get(); 126 | $this->assertEquals(6, count($users)); 127 | 128 | $users = User::where('name', 'NOT ilike', 't%')->get(); 129 | $this->assertEquals(8, count($users)); 130 | } 131 | 132 | public function testSelect() 133 | { 134 | $user = User::where('name', 'John Doe')->select('name')->first(); 135 | 136 | $this->assertEquals('John Doe', $user->name); 137 | $this->assertEquals(null, $user->age); 138 | $this->assertEquals(null, $user->title); 139 | 140 | $user = User::where('name', 'John Doe')->select('name', 'title')->first(); 141 | 142 | $this->assertEquals('John Doe', $user->name); 143 | $this->assertEquals('admin', $user->title); 144 | $this->assertEquals(null, $user->age); 145 | 146 | $user = User::where('name', 'John Doe')->select(['name', 'title'])->get()->first(); 147 | 148 | $this->assertEquals('John Doe', $user->name); 149 | $this->assertEquals('admin', $user->title); 150 | $this->assertEquals(null, $user->age); 151 | 152 | $user = User::where('name', 'John Doe')->get(['name'])->first(); 153 | 154 | $this->assertEquals('John Doe', $user->name); 155 | $this->assertEquals(null, $user->age); 156 | } 157 | 158 | public function testOrWhere() 159 | { 160 | $users = User::where('age', 13)->orWhere('title', 'admin')->get(); 161 | $this->assertEquals(4, count($users)); 162 | 163 | $users = User::where('age', 13)->orWhere('age', 23)->get(); 164 | $this->assertEquals(2, count($users)); 165 | } 166 | 167 | public function testBetween() 168 | { 169 | $users = User::whereBetween('age', [0, 25])->get(); 170 | $this->assertEquals(2, count($users)); 171 | 172 | $users = User::whereBetween('age', [13, 23])->get(); 173 | $this->assertEquals(2, count($users)); 174 | 175 | // testing whereNotBetween for version 4.1 176 | $users = User::whereBetween('age', [0, 25], 'and', true)->get(); 177 | $this->assertEquals(6, count($users)); 178 | } 179 | 180 | public function testIn() 181 | { 182 | $users = User::whereIn('age', [13, 23])->get(); 183 | $this->assertEquals(2, count($users)); 184 | 185 | $users = User::whereIn('age', [33, 35, 13])->get(); 186 | $this->assertEquals(6, count($users)); 187 | 188 | $users = User::whereNotIn('age', [33, 35])->get(); 189 | $this->assertEquals(4, count($users)); 190 | 191 | $users = User::whereNotNull('age') 192 | ->whereNotIn('age', [33, 35])->get(); 193 | $this->assertEquals(3, count($users)); 194 | } 195 | 196 | public function testWhereNull() 197 | { 198 | $users = User::whereNull('age')->get(); 199 | $this->assertEquals(1, count($users)); 200 | } 201 | 202 | public function testWhereNotNull() 203 | { 204 | $users = User::whereNotNull('age')->get(); 205 | $this->assertEquals(8, count($users)); 206 | } 207 | 208 | public function testOrder() 209 | { 210 | $user = User::whereNotNull('age')->orderBy('age', 'asc')->first(); 211 | 212 | $this->assertEquals(13, $user->age); 213 | 214 | $user = User::whereNotNull('age')->orderBy('age', 'ASC')->first(); 215 | $this->assertEquals(13, $user->age); 216 | 217 | $user = User::whereNotNull('age')->orderBy('age', 'desc')->first(); 218 | $this->assertEquals(37, $user->age); 219 | 220 | $this->expectException(\Exception::class); 221 | $user = User::whereNotNull('age')->orderBy('age', 'asc')->orderBy('name', 'desc')->first(); 222 | } 223 | 224 | public function testGroupBy() 225 | { 226 | $this->markTestSkipped('groupby not implemented yet'); 227 | 228 | $users = User::groupBy('title')->get(); 229 | $this->assertEquals(3, count($users)); 230 | 231 | $users = User::groupBy('age')->get(); 232 | $this->assertEquals(6, count($users)); 233 | 234 | $users = User::groupBy('age')->skip(1)->get(); 235 | $this->assertEquals(5, count($users)); 236 | 237 | $users = User::groupBy('age')->take(2)->get(); 238 | $this->assertEquals(2, count($users)); 239 | 240 | $users = User::groupBy('age')->orderBy('age', 'desc')->get(); 241 | $this->assertEquals(37, $users[0]->age); 242 | $this->assertEquals(35, $users[1]->age); 243 | $this->assertEquals(33, $users[2]->age); 244 | 245 | $users = User::groupBy('age')->skip(1)->take(2)->orderBy('age', 'desc')->get(); 246 | $this->assertEquals(2, count($users)); 247 | $this->assertEquals(35, $users[0]->age); 248 | $this->assertEquals(33, $users[1]->age); 249 | $this->assertNull($users[0]->name); 250 | 251 | $users = User::select('name')->groupBy('age')->skip(1)->take(2)->orderBy('age', 'desc')->get(); 252 | $this->assertEquals(2, count($users)); 253 | $this->assertNotNull($users[0]->name); 254 | } 255 | 256 | public function testCount() 257 | { 258 | $count = User::where('age', '<>', 35)->count(); 259 | $this->assertEquals(6, $count); 260 | 261 | $count = User::select('_id', 'age', 'title')->where('age', '<>', 35)->count(); 262 | $this->assertEquals(6, $count); 263 | } 264 | 265 | public function testExists() 266 | { 267 | $this->assertFalse(User::where('age', '>', 37)->exists()); 268 | $this->assertTrue(User::where('age', '<', 37)->exists()); 269 | } 270 | 271 | public function testSubquery() 272 | { 273 | $users = User::where('title', 'admin')->orWhere(function ($query) { 274 | $query->where('name', 'Tommy Toe') 275 | ->orWhere('name', 'Error'); 276 | }) 277 | ->get(); 278 | 279 | $this->assertEquals(5, count($users)); 280 | 281 | $users = User::where('title', 'user')->where(function ($query) { 282 | $query->where('age', 35) 283 | ->orWhere('name', 'ilike', '%harry%'); 284 | }) 285 | ->get(); 286 | 287 | $this->assertEquals(2, count($users)); 288 | 289 | $users = User::where('age', 35)->orWhere(function ($query) { 290 | $query->where('title', 'admin') 291 | ->orWhere('name', 'Error'); 292 | }) 293 | ->get(); 294 | 295 | $this->assertEquals(5, count($users)); 296 | 297 | $users = User::where('title', 'admin') 298 | ->where(function ($query) { 299 | $query->where('age', '>', 15) 300 | ->orWhere('name', 'Harry Hoe'); 301 | }) 302 | ->get(); 303 | 304 | $this->assertEquals(3, $users->count()); 305 | 306 | $users = User::where(function ($query) { 307 | $query->where('name', 'Harry Hoe') 308 | ->orWhere(function ($query) { 309 | $query->where('age', '>', 15) 310 | ->where('title', '<>', 'admin'); 311 | }); 312 | }) 313 | ->get(); 314 | 315 | $this->assertEquals(5, $users->count()); 316 | } 317 | 318 | public function testWhereRaw() 319 | { 320 | $where = ['age' => ['$gt' => 30, '$lt' => 40]]; 321 | $users = User::whereRaw($where)->get(); 322 | 323 | $this->assertEquals(6, count($users)); 324 | 325 | $where1 = ['age' => ['$gt' => 30, '$lte' => 35]]; 326 | $where2 = ['age' => ['$gt' => 35, '$lt' => 40]]; 327 | $users = User::whereRaw($where1)->orWhereRaw($where2)->get(); 328 | 329 | $this->assertEquals(6, count($users)); 330 | } 331 | 332 | public function testMultipleOr() 333 | { 334 | $users = User::where(function ($query) { 335 | $query->where('age', 35)->orWhere('age', 33); 336 | }) 337 | ->where(function ($query) { 338 | $query->where('name', 'John Doe')->orWhere('name', 'Jane Doe'); 339 | })->get(); 340 | 341 | $this->assertEquals(2, count($users)); 342 | 343 | $users = User::where(function ($query) { 344 | $query->orWhere('age', 35)->orWhere('age', 33); 345 | }) 346 | ->where(function ($query) { 347 | $query->orWhere('name', 'John Doe')->orWhere('name', 'Jane Doe'); 348 | })->get(); 349 | 350 | $this->assertEquals(2, count($users)); 351 | } 352 | 353 | public function testPaginate() 354 | { 355 | $this->markTestSkipped('paginate not implemented yet'); 356 | 357 | $results = User::paginate(2); 358 | $this->assertEquals(2, $results->count()); 359 | $this->assertNotNull($results->first()->title); 360 | $this->assertEquals(9, $results->total()); 361 | 362 | $results = User::paginate(2, ['name', 'age']); 363 | $this->assertEquals(2, $results->count()); 364 | $this->assertNull($results->first()->title); 365 | $this->assertEquals(9, $results->total()); 366 | $this->assertEquals(1, $results->currentPage()); 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /tests/QueueTest.php: -------------------------------------------------------------------------------- 1 | table(Config::get('queue.connections.database.table'))->truncate(); 11 | Queue::getDatabase()->table(Config::get('queue.failed.table'))->truncate(); 12 | } 13 | 14 | public function testQueueJobLifeCycle() 15 | { 16 | list($id,$rev) = Queue::push('test', ['action' => 'QueueJobLifeCycle'], 'test'); 17 | $this->assertNotNull($id); 18 | 19 | // Get and reserve the test job (next available) 20 | $job = Queue::pop('test'); 21 | $this->assertInstanceOf(Robsonvn\CouchDB\Queue\CouchJob::class, $job); 22 | $this->assertEquals(1, $job->isReserved()); 23 | $this->assertEquals(json_encode([ 24 | 'displayName' => 'test', 25 | 'job' => 'test', 26 | 'maxTries' => null, 27 | 'timeout' => null, 28 | 'data' => ['action' => 'QueueJobLifeCycle'], 29 | ]), $job->getRawBody()); 30 | 31 | // Remove reserved job 32 | $job->delete(); 33 | $this->assertEquals(0, Queue::getDatabase()->table(Config::get('queue.connections.database.table'))->count()); 34 | } 35 | 36 | public function testQueueJobExpired() 37 | { 38 | list($id,$rev) = Queue::push('test', ['action' => 'QueueJobExpired'], 'test'); 39 | 40 | $this->assertNotNull($id); 41 | 42 | // Expire the test job 43 | $expiry = \Carbon\Carbon::now()->subSeconds(Config::get('queue.connections.database.expire'))->getTimestamp(); 44 | Queue::getDatabase() 45 | ->table(Config::get('queue.connections.database.table')) 46 | ->where('_id', $id) 47 | ->update(['reserved' => 1, 'reserved_at' => $expiry]); 48 | 49 | // Expect an attempted older job in the queue 50 | $job = Queue::pop('test'); 51 | 52 | $this->assertEquals(1, $job->attempts()); 53 | $this->assertGreaterThan($expiry, $job->reservedAt()); 54 | 55 | $job->delete(); 56 | $this->assertEquals(0, Queue::getDatabase()->table(Config::get('queue.connections.database.table'))->count()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/SeederTest.php: -------------------------------------------------------------------------------- 1 | run(); 14 | 15 | $user = User::where('name', 'John Doe')->first(); 16 | $this->assertTrue($user->seed); 17 | } 18 | 19 | public function testArtisan() 20 | { 21 | Artisan::call('db:seed'); 22 | 23 | $user = User::where('name', 'John Doe')->first(); 24 | $this->assertTrue($user->seed); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('app.key', 'ZsZewWyUJ5FsKp9lMwv4tYbNlegQilM7'); 33 | 34 | $app['config']->set('database.default', 'couchdb'); 35 | $app['config']->set('database.connections.mysql', $config['connections']['mysql']); 36 | $app['config']->set('database.connections.couchdb', $config['connections']['couchdb']); 37 | 38 | $app['config']->set('auth.model', 'User'); 39 | $app['config']->set('auth.providers.users.model', 'User'); 40 | $app['config']->set('cache.driver', 'array'); 41 | 42 | $app['config']->set('queue.default', 'database'); 43 | $app['config']->set('queue.connections.database', [ 44 | 'driver' => 'couchdb', 45 | 'table' => 'jobs', 46 | 'queue' => 'default', 47 | 'expire' => 60, 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/ValidationTest.php: -------------------------------------------------------------------------------- 1 | 'John Doe'], 14 | ['name' => 'required|unique:users'] 15 | ); 16 | $this->assertFalse($validator->fails()); 17 | 18 | User::create(['name' => 'John Doe']); 19 | 20 | $validator = Validator::make( 21 | ['name' => 'John Doe'], 22 | ['name' => 'required|unique:users'] 23 | ); 24 | $this->assertTrue($validator->fails()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/config/database.php: -------------------------------------------------------------------------------- 1 | [ 6 | 7 | 'couchdb' => [ 8 | 'name' => 'couchdb', 9 | 'type' => 'socket', 10 | 'driver' => 'couchdb', 11 | 'host' => getenv('COUCHDB_HOST','localhost'), 12 | 'dbname' => getenv('COUCHDB_DB_NAME','test'), 13 | ], 14 | 15 | 'mysql' => [ 16 | 'driver' => 'mysql', 17 | 'host' => '127.0.0.1', 18 | 'database' => 'unittest', 19 | 'username' => 'travis', 20 | 'password' => '', 21 | 'charset' => 'utf8', 22 | 'collation' => 'utf8_unicode_ci', 23 | 'prefix' => '', 24 | ], 25 | ], 26 | 27 | ]; 28 | -------------------------------------------------------------------------------- /tests/config/queue.php: -------------------------------------------------------------------------------- 1 | 'database', 6 | 7 | 'connections' => [ 8 | 9 | 'database' => [ 10 | 'driver' => 'couchdb', 11 | 'table' => 'jobs', 12 | 'queue' => 'default', 13 | 'expire' => 60, 14 | ], 15 | 16 | ], 17 | 18 | 'failed' => [ 19 | 'database' => 'couchdb', 20 | 'table' => 'failed_jobs', 21 | ], 22 | 23 | ]; 24 | -------------------------------------------------------------------------------- /tests/models/Address.php: -------------------------------------------------------------------------------- 1 | embedsMany('Address'); 12 | } 13 | 14 | public function country() 15 | { 16 | return $this->belongsTo('Country'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/models/Book.php: -------------------------------------------------------------------------------- 1 | belongsTo('User', 'author_id'); 13 | } 14 | 15 | public function mysqlAuthor() 16 | { 17 | return $this->belongsTo('MysqlUser', 'author_id'); 18 | } 19 | 20 | public function tags() 21 | { 22 | return $this->morphToMany('Tag', 'book','book_tag','book_id','tag_id'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/models/Client.php: -------------------------------------------------------------------------------- 1 | belongsToMany('User'); 13 | } 14 | 15 | public function photo() 16 | { 17 | return $this->morphOne('Photo', 'imageable'); 18 | } 19 | 20 | public function addresses() 21 | { 22 | return $this->hasMany('Address', 'data.client_id', 'data.client_id'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/models/Company.php: -------------------------------------------------------------------------------- 1 | hasMany('User'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/models/Country.php: -------------------------------------------------------------------------------- 1 | belongsToMany('User', null, 'groups', 'users'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/models/Item.php: -------------------------------------------------------------------------------- 1 | belongsTo('User'); 13 | } 14 | 15 | public function scopeSharp($query) 16 | { 17 | return $query->where('object_type', 'sharp'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/models/Movie.php: -------------------------------------------------------------------------------- 1 | morphToMany('Tag', 'taggable'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/models/Photo.php: -------------------------------------------------------------------------------- 1 | morphTo(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/models/Role.php: -------------------------------------------------------------------------------- 1 | belongsTo('User'); 13 | } 14 | 15 | public function mysqlUser() 16 | { 17 | return $this->belongsTo('MysqlUser'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/models/Soft.php: -------------------------------------------------------------------------------- 1 | morphedByMany('Movie', 'taggable'); 12 | } 13 | 14 | public function books() 15 | { 16 | return $this->morphedByMany('Book', 'book','book_tag','tag_id','book_id'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/models/User.php: -------------------------------------------------------------------------------- 1 | hasMany('Book', 'author_id'); 17 | } 18 | 19 | public function mysqlBooks() 20 | { 21 | return $this->hasMany('MysqlBook', 'author_id'); 22 | } 23 | 24 | public function items() 25 | { 26 | return $this->hasMany('Item'); 27 | } 28 | 29 | public function role() 30 | { 31 | return $this->hasOne('Role'); 32 | } 33 | 34 | public function mysqlRole() 35 | { 36 | return $this->hasOne('MysqlRole'); 37 | } 38 | 39 | public function clients() 40 | { 41 | return $this->belongsToMany('Client'); 42 | } 43 | 44 | public function groups() 45 | { 46 | return $this->belongsToMany('Group', null, 'users', 'groups'); 47 | } 48 | 49 | public function photos() 50 | { 51 | return $this->morphMany('Photo', 'imageable'); 52 | } 53 | 54 | public function addresses() 55 | { 56 | return $this->embedsMany('Address'); 57 | } 58 | 59 | public function father() 60 | { 61 | return $this->embedsOne('User'); 62 | } 63 | 64 | public function company() 65 | { 66 | return $this->belongsTo('Company'); 67 | } 68 | 69 | protected function getDateFormat() 70 | { 71 | return 'l jS \of F Y h:i:s A'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call('UserTableSeeder'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/seeds/UserTableSeeder.php: -------------------------------------------------------------------------------- 1 | delete(); 11 | 12 | DB::collection('users')->insert(['name' => 'John Doe', 'seed' => true]); 13 | } 14 | } 15 | --------------------------------------------------------------------------------