├── .github └── workflows │ └── test.yml ├── .gitignore ├── .php_cs ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── readme └── era.png ├── src ├── EloquentJoinBuilder.php ├── Exceptions │ ├── InvalidAggregateMethod.php │ ├── InvalidDirection.php │ ├── InvalidRelation.php │ ├── InvalidRelationClause.php │ ├── InvalidRelationGlobalScope.php │ └── InvalidRelationWhere.php ├── Relations │ ├── BelongsToJoin.php │ ├── HasManyJoin.php │ └── HasOneJoin.php └── Traits │ ├── EloquentJoin.php │ ├── ExtendRelationsTrait.php │ └── JoinRelationTrait.php └── tests ├── Models ├── BaseModel.php ├── City.php ├── Integration.php ├── Key │ ├── Location.php │ ├── Order.php │ └── Seller.php ├── Location.php ├── LocationAddress.php ├── LocationWithGlobalScope.php ├── Order.php ├── OrderItem.php ├── Seller.php ├── State.php ├── User.php └── ZipCode.php ├── Scope └── TestExceptionScope.php ├── ServiceProvider.php ├── TestCase.php ├── Tests ├── AggregateJoinTest.php ├── AppendRelationsCountTest.php ├── Clauses │ ├── JoinRelationsTest.php │ ├── OrWhereInTest.php │ ├── OrWhereNotInTest.php │ ├── OrWhereTest.php │ ├── OrderByTest.php │ ├── WhereInTest.php │ ├── WhereNotInTest.php │ └── WhereTest.php ├── ClosureOnRelationTest.php ├── ClosureTest.php ├── ExceptionTest.php ├── JoinTypeTest.php ├── KeysOwnerTest.php ├── KeysTest.php ├── OptionsTest.php ├── Relations │ ├── BelongsToTest.php │ ├── HasManyTest.php │ └── HasOneTest.php └── SoftDeleteTest.php └── database └── migrations └── 2017_11_04_163552_create_database.php /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php_version: [7.4, 8.0, 8.1] 15 | laravel_version: [8.*, 9.*] 16 | 17 | steps: 18 | - name: Checkout commit 19 | uses: actions/checkout@v2 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php_version }} 25 | 26 | - name: Validate composer.json 27 | run: composer validate 28 | 29 | - name: Run composer install 30 | run: composer install --no-interaction --no-suggest 31 | 32 | - name: Run find-and-replace to replace * with 0 33 | uses: mad9000/actions-find-and-replace-string@1 34 | id: laravel_version_cleaned 35 | with: 36 | source: ${{ matrix.laravel_version }} 37 | find: '*' 38 | replace: '0' 39 | 40 | - name: Install Laravel 41 | run: composer update --no-interaction illuminate/database:^${{ steps.laravel_version_cleaned.outputs.value }} 42 | 43 | - name: Run PHPUnit 44 | run: ./vendor/bin/phpunit 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.lock 4 | .php_cs.cache 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in([ 11 | __DIR__ .'/src', 12 | __DIR__ .'/tests', 13 | ]); 14 | ; 15 | 16 | /* 17 | * Do the magic 18 | */ 19 | return Config::create() 20 | ->setUsingCache(false) 21 | ->setRules([ 22 | '@PSR2' => true, 23 | '@Symfony' => true, 24 | ]) 25 | ->setFinder($finder) 26 | ; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Filip Horvat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tests status](https://github.com/fico7489/laravel-eloquent-join/workflows/Test/badge.svg) 2 | 3 | # Laravel Eloquent Join 4 | 5 | This package introduces the join magic for eloquent models and relations. 6 | 7 | ## Introduction 8 | 9 | Eloquent is a powerful ORM but its join capabilities are very poor. 10 | 11 | #### First Eloquent Problem (sorting) 12 | 13 | With laravel you can't perform sorting of the relationship fields without manually joining related table which is very awkward. Let me give you a few reasons why. If you have a table with **posts** and related **categories** your code might look like this: 14 | 15 | ``` 16 | $posts = Post::select('posts.*') 17 | ->join('categories', 'categories.id', '=', 'posts.category_id') 18 | ->groupBy('posts.id') 19 | ->where('categories.deleted_at', '=', null) 20 | ->orderBy('categories.name'); 21 | 22 | if(request()->get('date')){ 23 | $posts->where('posts.date', $date) 24 | } 25 | 26 | $posts = $posts->get(); 27 | ``` 28 | 29 | 1.The first problem is that you need to worry about select. 30 | ``` 31 | ->select('posts.*') 32 | ``` 33 | Reason : without **select** id from the category can be selected and hydrated into the Post model. 34 | 35 | 2.The second problem is that you need to worry about **groupBy**. 36 | 37 | ->groupBy('posts.id'); 38 | 39 | Reason : if the relation is HasOne and there are more than one categories for the post, the query will return more rows for categories. 40 | 41 | 3.The third problem is that you need to change all other where clauses from : 42 | ``` 43 | ->where('date', $date) 44 | ``` 45 | to 46 | ``` 47 | ->where('posts.date', $date) 48 | ``` 49 | Reason : a **post** and **category** can have "date" attribute and in that case without selecting an attribute with table "ambiguous column" error will be thrown. 50 | 51 | 4.The fourth problem is that you are using table names(not models) and this is also bad and awkward. 52 | ``` 53 | ->where('posts.date', $date) 54 | ``` 55 | 5.The fifth problem is that you need to worry about soft deletes for joined tables. If the **category** is using SoftDeletes trait you must add : 56 | ``` 57 | ->where('categories.deleted_at', '=', null) 58 | ``` 59 | This package will take care of all above problems for you. 60 | Unlike **sorting**, you can perform **filtering** on the relationship fields without joining related tables, but this package will give you the ability to do this easier. 61 | 62 | 63 | #### Second Eloquent Problem (subqueries) 64 | 65 | With laravel you can perform where on the relationship attribute but laravel will generate subqueries which are more slower than joins. 66 | With this package you will be available to perform where on the relationship with joins in an elegant way. 67 | 68 | 69 | ## Requirements 70 | 71 | | Laravel Version | Package Tag | Supported | Development Branch 72 | |-----------------|-------------|-----------| -----------| 73 | | >= 5.5.0 | 4.* | yes | master 74 | | < 5.5.0 | - | no | - 75 | 76 | Package is also tested for SQLite, MySql and PostgreSql 77 | 78 | ## Installation & setup 79 | 80 | 1.Install package with composer 81 | ``` 82 | composer require fico7489/laravel-eloquent-join 83 | ``` 84 | With this statement, a composer will install highest available package version for your current laravel version. 85 | 86 | 2.Use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoinTrait trait in your base model or only in particular models. 87 | 88 | ``` 89 | ... 90 | use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoin; 91 | use Illuminate\Database\Eloquent\Model; 92 | 93 | abstract class BaseModel extends Model 94 | { 95 | use EloquentJoin; 96 | ... 97 | ``` 98 | 99 | 3.IMPORTANT 100 | 101 | For **MySql** make sure that **strict** configuration is set to **false** 102 | 103 | config/database.php 104 | 105 | ``` 106 | 'mysql' => [ 107 | ... 108 | 'strict' => false, 109 | ... 110 | ``` 111 | 112 | and that's it, you are ready to go. 113 | 114 | ## Options 115 | 116 | Options can be set in the model : 117 | 118 | ``` 119 | class Seller extends BaseModel 120 | { 121 | protected $useTableAlias = false; 122 | protected $appendRelationsCount = false; 123 | protected $leftJoin = false; 124 | protected $aggregateMethod = 'MAX'; 125 | ``` 126 | 127 | or on query : 128 | 129 | ``` 130 | Order::setUseTableAlias(true)->get(); 131 | Order::setAppendRelationsCount(true)->get(); 132 | Order::setLeftJoin(true)->get(); 133 | Order::setAggregateMethod(true)->get(); 134 | ``` 135 | 136 | #### **useTableAlias** 137 | 138 | Should we use an alias for joined tables (default = false) 139 | 140 | With **true** query will look like this : 141 | ``` 142 | select "sellers".* from "sellers" 143 | left join "locations" as "5b5c093d2e00f" 144 | ... 145 | ``` 146 | 147 | With **false** query will look like this : 148 | ``` 149 | select "sellers".* 150 | from "sellers" 151 | left join "locations" 152 | ... 153 | ``` 154 | 155 | Alias is a randomly generated string. 156 | 157 | #### **appendRelationsCount** 158 | 159 | Should we automatically append relation count field to results (default = false) 160 | 161 | With **true** query will look like this : 162 | ``` 163 | select "sellers".*, count(locations.id) AS locations_count 164 | from "sellers" 165 | left join "locations" as "5b5c093d2e00f" 166 | ... 167 | ``` 168 | 169 | Each **relation** is glued with an underscore and at the end **_count** prefix is added. For example for 170 | 171 | ->joinRelations('seller.locations') 172 | 173 | field would be __seller_locations_count__ 174 | 175 | #### **leftJoin** 176 | 177 | Should we use **inner join** or **left join** (default = true) 178 | 179 | ``` 180 | select "sellers".* 181 | from "sellers" 182 | inner join "locations" 183 | ... 184 | ``` 185 | 186 | vs 187 | 188 | ``` 189 | select "sellers".* 190 | from "sellers" 191 | left join "locations" 192 | ... 193 | ``` 194 | 195 | #### **aggregateMethod** 196 | 197 | Which aggregate method to use for ordering (default = 'MAX'). 198 | 199 | When join is performed on the joined table we must apply aggregate functions on the sorted field so we could perform group by clause and prevent duplication of results. 200 | 201 | ``` 202 | select "sellers".*, MAX("locations" ."number") AS sort 203 | from "sellers" 204 | left join "locations" 205 | group by "locations" ."id" 206 | order by sort 207 | ... 208 | ``` 209 | 210 | Options are : **SUM**, **AVG**, **MAX**, **MIN**, **COUNT** 211 | 212 | ## Usage 213 | 214 | ### Currently available relations for join queries 215 | 216 | * **BelongsTo** 217 | * **HasOne** 218 | * **HasMany** 219 | 220 | ### New clauses for eloquent builder on BelongsTo and HasOne relations : 221 | 222 | **joinRelations($relations, $leftJoin = null)** 223 | 224 | * ***$relations*** which relations to join 225 | * ***$leftJoin*** use **left join** or **inner join**, default **left join** 226 | 227 | **orderByJoin($column, $direction = 'asc', $aggregateMethod = null)** 228 | 229 | * ***$column*** and ***$direction*** arguments are the same as in default eloquent **orderBy()** 230 | * ***$aggregateMethod*** argument defines which aggregate method to use ( **SUM**, **AVG**, **MAX**, **MIN**, **COUNT**), default **MAX** 231 | 232 | **whereJoin($column, $operator, $value, $boolean = 'and')** 233 | 234 | * arguments are the same as in default eloquent **where()** 235 | 236 | **orWhereJoin($column, $operator, $value)** 237 | 238 | * arguments are the same as in default eloquent **orWhere()** 239 | 240 | 241 | **whereInJoin($column, $values, $boolean = 'and', $not = false)** 242 | 243 | * arguments are the same as in default eloquent **whereIn()** 244 | 245 | **whereNotInJoin($column, $values, $boolean = 'and')** 246 | 247 | * arguments are the same as in default eloquent **whereNotIn()** 248 | 249 | **orWhereInJoin($column, $values)** 250 | 251 | * arguments are the same as in default eloquent **orWhereIn()** 252 | 253 | **orWhereNotInJoin($column, $values)** 254 | 255 | * arguments are the same as in default eloquent **orWhereNotIn()** 256 | 257 | 258 | ### Allowed clauses on BelongsTo, HasOne and HasMany relations on which you can use join clauses on the query 259 | 260 | * Relations that you want to use for join queries can only have these clauses : **where**, **orWhere**, **withTrashed**, **onlyTrashed**, **withoutTrashed**. 261 | * Clauses **where** and **orWhere** can only have these variations 262 | ** **->where($column, $operator, $value)** 263 | ** **->where([$column => $value])** 264 | * Closures are not allowed. 265 | * Other clauses like **whereHas**, **orderBy** etc. are not allowed. 266 | * You can add not allowed clauses on relations and use them in the normal eloquent way, but in these cases, you can't use those relations for join queries. 267 | 268 | Allowed relation: 269 | 270 | ``` 271 | public function locationPrimary() 272 | { 273 | return $this->hasOne(Location::class) 274 | ->where('is_primary', '=', 1) 275 | ->orWhere('is_primary', '=', 1) 276 | ->withTrashed(); 277 | } 278 | ``` 279 | Not allowed relation: 280 | 281 | ``` 282 | public function locationPrimary() 283 | { 284 | return $this->hasOne(Location::class) 285 | ->where('is_primary', '=', 1) 286 | ->orWhere('is_primary', '=', 1) 287 | ->withTrashed() 288 | ->whereHas('state', function($query){return $query;} 289 | ->orderBy('name') 290 | ->where(function($query){ 291 | return $query->where('is_primary', '=', 1); 292 | }); 293 | } 294 | ``` 295 | 296 | The reason why the second relation is not allowed is that this package should apply all those clauses on the join clause, eloquent use all those clauses isolated with subqueries NOT on join clause and that is more simpler to do. 297 | 298 | You might get a picture that there are too many rules and restriction, but it is really not like that. 299 | Don't worry, if you do create the query that is not allowed appropriate exception will be thrown and you will know what happened. 300 | 301 | ### Other 302 | 303 | * If the model uses the SoftDelete trait, where deleted_at != null will be automatically applied 304 | * You can combine new clauses unlimited times 305 | * If you combine clauses more times on same relation package will join related table only once 306 | 307 | ``` 308 | Seller::whereJoin('city.title', '=', 'test') 309 | ->orWhereJoin('city.title', '=', 'test2'); 310 | ``` 311 | 312 | * You can call new clauses inside closures 313 | 314 | ``` 315 | Seller::where(function ($query) { 316 | $query 317 | ->whereJoin('city.title', '=', 'test') 318 | ->orWhereJoin('city.title', '=', 'test2'); 319 | }); 320 | ``` 321 | 322 | * You can combine join clauses e.g. whereJoin() with eloquent clauses e.g. orderBy() 323 | 324 | ``` 325 | Seller::whereJoin('title', '=', 'test') 326 | ->whereJoin('city.title', '=', 'test') 327 | ->orderByJoin('city.title') 328 | ->get(); 329 | ``` 330 | 331 | ## See action on real example 332 | 333 | Database schema : 334 | 335 | ![Database schema](https://raw.githubusercontent.com/fico7489/laravel-eloquent-join/master/readme/era.png) 336 | 337 | Models : 338 | 339 | ``` 340 | class Seller extends BaseModel 341 | { 342 | public function locations() 343 | { 344 | return $this->hasMany(Location::class); 345 | } 346 | 347 | public function locationPrimary() 348 | { 349 | return $this->hasOne(Location::class) 350 | ->where('is_primary', '=', 1); 351 | } 352 | 353 | public function city() 354 | { 355 | return $this->belongsTo(City::class); 356 | } 357 | ``` 358 | ``` 359 | class Location extends BaseModel 360 | { 361 | public function locationAddressPrimary() 362 | { 363 | return $this->hasOne(LocationAddress::class) 364 | ->where('is_primary', '=', 1); 365 | } 366 | 367 | ``` 368 | ``` 369 | class City extends BaseModel 370 | { 371 | public function state() 372 | { 373 | return $this->belongsTo(State::class); 374 | } 375 | } 376 | ``` 377 | 378 | ### Join 379 | 380 | ##### Join BelongsTo 381 | ```Seller::joinRelations('city')``` 382 | 383 | ##### Join HasOne 384 | ```Seller::joinRelations('locationPrimary')``` 385 | 386 | ##### Join HasMany 387 | ```Seller::joinRelations('locations')``` 388 | 389 | ##### Join Mixed 390 | ```Seller::joinRelations('city.state')``` 391 | 392 | ### Join (mix left join) 393 | 394 | ```Seller::joinRelations('city', true)->joinRelations('city.state', false)``` 395 | 396 | ### Join (multiple relationships) 397 | 398 | ```Seller::join(['city.state', 'locations'])``` 399 | 400 | ### Ordering 401 | 402 | ##### Order BelongsTo 403 | ```Seller::orderByJoin('city.title')``` 404 | 405 | ##### Order HasOne 406 | ```Seller::orderByJoin('locationPrimary.address')``` 407 | 408 | ##### Order HasMany 409 | ```Seller::orderByJoin('locations.title')``` 410 | 411 | ##### Order Mixed 412 | ```Seller::orderByJoin('city.state.title')``` 413 | 414 | ### Ordering (special cases with aggregate functions) 415 | 416 | ##### Order by relation count 417 | ```Seller::orderByJoin('locations.id', 'asc', 'COUNT')``` 418 | 419 | ##### Order by relation field SUM 420 | ```Seller::orderByJoin('locations.is_primary', 'asc', 'SUM')``` 421 | 422 | ##### Order by relation field AVG 423 | ```Seller::orderByJoin('locations.is_primary', 'asc', 'AVG')``` 424 | 425 | ##### Order by relation field MAX 426 | ```Seller::orderByJoin('locations.is_primary', 'asc', 'MAX')``` 427 | 428 | ##### Order by relation field MIN 429 | ```Seller::orderByJoin('locations.is_primary', 'asc', 'MIN')``` 430 | 431 | ### Filtering (where or orWhere) 432 | 433 | ##### Filter BelongsTo 434 | ```Seller::whereJoin('city.title', '=', 'test')``` 435 | 436 | ##### Filter HasOne 437 | ```Seller::whereJoin('locationPrimary.address', '=', 'test')``` 438 | 439 | ##### Filter HasMany 440 | ```Seller::whereJoin('locations.title', '=', 'test')``` 441 | 442 | ##### Filter Mixed 443 | ```Seller::whereJoin('city.state.title', '=', 'test')``` 444 | 445 | ### Relation count 446 | 447 | ``` 448 | $sellers = Seller::setAppendRelationsCount(true)->join('locations', '=', 'test') 449 | ->get(); 450 | 451 | foreach ($sellers as $seller){ 452 | echo 'Number of location = ' . $seller->locations_count; 453 | } 454 | 455 | ``` 456 | 457 | ### Filter (mix left join) 458 | 459 | ``` 460 | Seller::joinRelations('city', true) 461 | ->joinRelations('city.state', false) 462 | ->whereJoin('city.id', '=', 1) 463 | ->orWhereJoin('city.state.id', '=', 1) 464 | ``` 465 | 466 | ## Generated queries 467 | 468 | Query : 469 | ``` 470 | Order::whereJoin('seller.id', '=', 1)->get(); 471 | ``` 472 | 473 | Sql : 474 | ``` 475 | select "orders".* 476 | from "orders" 477 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 478 | where "sellers"."id" = ? 479 | and "orders"."deleted_at" is null 480 | group by "orders"."id" 481 | ``` 482 | 483 | Query : 484 | ``` 485 | Order::orderByJoin('seller.id', '=', 1)->get(); 486 | ``` 487 | 488 | Sql : 489 | ``` 490 | select "orders".*, MAX(sellers.id) as sort 491 | from "orders" 492 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 493 | where "orders"."deleted_at" is null 494 | group by "orders"."id" 495 | order by sort asc 496 | ``` 497 | 498 | ## Elegance of package 499 | 500 | Lets look how first example from documentation now looks like. This code : 501 | 502 | ``` 503 | $posts = Post::select('posts.*') 504 | ->join('categories', 'categories.id', '=', 'posts.category_id') 505 | ->groupBy('posts.id') 506 | ->where('categories.deleted_at', '=', null) 507 | ->orderBy('categories.name'); 508 | 509 | if(request()->get('date')){ 510 | $posts->where('date', $date) 511 | } 512 | 513 | $posts = $posts->get(); 514 | ``` 515 | 516 | is now : 517 | 518 | ``` 519 | $posts = Post::orderByJoin('category.name'); 520 | 521 | if(request()->get('date')){ 522 | $posts->where('posts.date', $date) 523 | } 524 | 525 | $posts = $posts->get(); 526 | ``` 527 | 528 | Both snippets do the same thing. 529 | 530 | ## Tests 531 | 532 | This package is well covered with tests. If you want run tests just run **composer update** and then run tests with **"vendor/bin/phpunit"** 533 | 534 | ## Contribution 535 | 536 | Feel free to create new issue for : 537 | * bug 538 | * notice 539 | * request new feature 540 | * question 541 | * clarification 542 | * etc... 543 | 544 | 545 | 546 | License 547 | ---- 548 | 549 | MIT 550 | 551 | 552 | **Free Software, Hell Yeah!** 553 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fico7489/laravel-eloquent-join", 3 | "description": "This package introduces the join magic for eloquent models and relations.", 4 | "keywords": [ 5 | "laravel join", 6 | "laravel eloquent join", 7 | "laravel sort join", 8 | "laravel where join", 9 | "laravel join relation" 10 | ], 11 | "homepage": "https://github.com/fico7489/laravel-eloquent-join", 12 | "support": { 13 | "issues": "https://github.com/fico7489/laravel-eloquent-join/issues", 14 | "source": "https://github.com/fico7489/laravel-eloquent-join" 15 | }, 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Filip Horvat", 20 | "email": "filip.horvat@am2studio.hr", 21 | "homepage": "http://am2studio.hr", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "illuminate/database": "^8.0|^9.0" 27 | }, 28 | "require-dev": { 29 | "orchestra/testbench": "*", 30 | "friendsofphp/php-cs-fixer" : "*", 31 | "phpunit/phpunit": "*" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Fico7489\\Laravel\\EloquentJoin\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Fico7489\\Laravel\\EloquentJoin\\Tests\\": "tests/" 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true 45 | } 46 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ./tests/ 22 | 23 | 24 | 25 | 26 | 27 | ./src 28 | 29 | ./tests 30 | ./vendor 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /readme/era.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebronner/laravel-eloquent-join/97371a30bc3670c86e507f4b20f9939778b82c66/readme/era.png -------------------------------------------------------------------------------- /src/EloquentJoinBuilder.php: -------------------------------------------------------------------------------- 1 | select(...) is already called on builder (we want only one groupBy()) 44 | protected $selected = false; 45 | 46 | //store joined tables, we want join table only once (e.g. when you call orderByJoin more time) 47 | protected $joinedTables = []; 48 | 49 | //store clauses on relation for join 50 | public $relationClauses = []; 51 | 52 | //query methods 53 | public function where($column, $operator = null, $value = null, $boolean = 'and') 54 | { 55 | if ($column instanceof \Closure) { 56 | $query = $this->model->newModelQuery(); 57 | $baseBuilderCurrent = $this->baseBuilder ? $this->baseBuilder : $this; 58 | $query->baseBuilder = $baseBuilderCurrent; 59 | 60 | $column($query); 61 | 62 | $this->query->addNestedWhereQuery($query->getQuery(), $boolean); 63 | } else { 64 | $this->query->where(...func_get_args()); 65 | } 66 | 67 | return $this; 68 | } 69 | 70 | public function whereJoin($column, $operator, $value, $boolean = 'and') 71 | { 72 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 73 | $column = $query->performJoin($column); 74 | 75 | return $this->where($column, $operator, $value, $boolean); 76 | } 77 | 78 | public function orWhereJoin($column, $operator, $value) 79 | { 80 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 81 | $column = $query->performJoin($column); 82 | 83 | return $this->orWhere($column, $operator, $value); 84 | } 85 | 86 | public function whereInJoin($column, $values, $boolean = 'and', $not = false) 87 | { 88 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 89 | $column = $query->performJoin($column); 90 | 91 | return $this->whereIn($column, $values, $boolean, $not); 92 | } 93 | 94 | public function whereNotInJoin($column, $values, $boolean = 'and') 95 | { 96 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 97 | $column = $query->performJoin($column); 98 | 99 | return $this->whereNotIn($column, $values, $boolean); 100 | } 101 | 102 | public function orWhereInJoin($column, $values) 103 | { 104 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 105 | $column = $query->performJoin($column); 106 | 107 | return $this->orWhereIn($column, $values); 108 | } 109 | 110 | public function orWhereNotInJoin($column, $values) 111 | { 112 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 113 | $column = $query->performJoin($column); 114 | 115 | return $this->orWhereNotIn($column, $values); 116 | } 117 | 118 | public function orderByJoin($column, $direction = 'asc', $aggregateMethod = null) 119 | { 120 | $direction = strtolower($direction); 121 | $this->checkDirection($direction); 122 | $dotPos = strrpos($column, '.'); 123 | 124 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 125 | $column = $query->performJoin($column); 126 | if (false !== $dotPos) { 127 | //order by related table field 128 | $aggregateMethod = $aggregateMethod ? $aggregateMethod : $this->aggregateMethod; 129 | $this->checkAggregateMethod($aggregateMethod); 130 | 131 | $sortsCount = count($this->query->orders ?? []); 132 | $sortAlias = 'sort'.(0 == $sortsCount ? '' : ($sortsCount + 1)); 133 | 134 | $grammar = \DB::query()->getGrammar(); 135 | $query->selectRaw($aggregateMethod.'('.$grammar->wrap($column).') as '.$sortAlias); 136 | 137 | return $this->orderByRaw($sortAlias.' '.$direction); 138 | } 139 | 140 | //order by base table field 141 | 142 | return $this->orderBy($column, $direction); 143 | } 144 | 145 | /** 146 | * Joining relations. 147 | * 148 | * @param string|array $relations 149 | * @param bool|null $leftJoin 150 | * 151 | * @return $this 152 | * 153 | * @throws InvalidRelation 154 | */ 155 | public function joinRelations($relations, $leftJoin = null) 156 | { 157 | $leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin; 158 | $query = $this->baseBuilder ? $this->baseBuilder : $this; 159 | 160 | if (is_array($relations)) { 161 | foreach ($relations as $relation) { 162 | $query->joinRelations($relation, $leftJoin); 163 | } 164 | } else { 165 | $query->performJoin($relations.'.FAKE_FIELD', $leftJoin); 166 | } 167 | 168 | return $this; 169 | } 170 | 171 | //helpers methods 172 | protected function performJoin($relations, $leftJoin = null) 173 | { 174 | //detect join method 175 | $leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin; 176 | $joinMethod = $leftJoin ? 'leftJoin' : 'join'; 177 | 178 | //detect current model data 179 | $relations = explode('.', $relations); 180 | $column = end($relations); 181 | $baseModel = $this->getModel(); 182 | $baseTable = $baseModel->getTable(); 183 | $basePrimaryKey = $baseModel->getKeyName(); 184 | 185 | $currentModel = $baseModel; 186 | $currentTableAlias = $baseTable; 187 | 188 | $relationsAccumulated = []; 189 | foreach ($relations as $relation) { 190 | if ($relation == $column) { 191 | //last item in $relations argument is sort|where column 192 | break; 193 | } 194 | 195 | /** @var Relation $relatedRelation */ 196 | $relatedRelation = $currentModel->$relation(); 197 | $relatedModel = $relatedRelation->getRelated(); 198 | $relatedPrimaryKey = $relatedModel->getKeyName(); 199 | $relatedTable = $relatedModel->getTable(); 200 | $relatedTableAlias = $this->useTableAlias ? sha1($relatedTable.rand()) : $relatedTable; 201 | 202 | $relationsAccumulated[] = $relatedTableAlias; 203 | $relationAccumulatedString = implode('_', $relationsAccumulated); 204 | 205 | //relations count 206 | if ($this->appendRelationsCount) { 207 | $this->selectRaw('COUNT('.$relatedTableAlias.'.'.$relatedPrimaryKey.') as '.$relationAccumulatedString.'_count'); 208 | } 209 | 210 | if (!in_array($relationAccumulatedString, $this->joinedTables)) { 211 | $joinQuery = $relatedTable.($this->useTableAlias ? ' as '.$relatedTableAlias : ''); 212 | if ($relatedRelation instanceof BelongsToJoin) { 213 | $relatedKey = is_callable([$relatedRelation, 'getQualifiedForeignKeyName']) ? $relatedRelation->getQualifiedForeignKeyName() : $relatedRelation->getQualifiedForeignKey(); 214 | $relatedKey = last(explode('.', $relatedKey)); 215 | $ownerKey = is_callable([$relatedRelation, 'getOwnerKeyName']) ? $relatedRelation->getOwnerKeyName() : $relatedRelation->getOwnerKey(); 216 | 217 | $this->$joinMethod($joinQuery, function ($join) use ($relatedRelation, $relatedTableAlias, $relatedKey, $currentTableAlias, $ownerKey) { 218 | $join->on($relatedTableAlias.'.'.$ownerKey, '=', $currentTableAlias.'.'.$relatedKey); 219 | 220 | $this->joinQuery($join, $relatedRelation, $relatedTableAlias); 221 | }); 222 | } elseif ($relatedRelation instanceof HasOneJoin || $relatedRelation instanceof HasManyJoin) { 223 | $relatedKey = $relatedRelation->getQualifiedForeignKeyName(); 224 | $relatedKey = last(explode('.', $relatedKey)); 225 | $localKey = $relatedRelation->getQualifiedParentKeyName(); 226 | $localKey = last(explode('.', $localKey)); 227 | 228 | $this->$joinMethod($joinQuery, function ($join) use ($relatedRelation, $relatedTableAlias, $relatedKey, $currentTableAlias, $localKey) { 229 | $join->on($relatedTableAlias.'.'.$relatedKey, '=', $currentTableAlias.'.'.$localKey); 230 | 231 | $this->joinQuery($join, $relatedRelation, $relatedTableAlias); 232 | }); 233 | } else { 234 | throw new InvalidRelation(); 235 | } 236 | } 237 | 238 | $currentModel = $relatedModel; 239 | $currentTableAlias = $relatedTableAlias; 240 | 241 | $this->joinedTables[] = implode('_', $relationsAccumulated); 242 | } 243 | 244 | if (!$this->selected && count($relations) > 1) { 245 | $this->selected = true; 246 | $this->selectRaw($baseTable.'.*'); 247 | $this->groupBy($baseTable.'.'.$basePrimaryKey); 248 | } 249 | 250 | return $currentTableAlias.'.'.$column; 251 | } 252 | 253 | protected function joinQuery($join, $relation, $relatedTableAlias) 254 | { 255 | /** @var Builder $relationQuery */ 256 | $relationBuilder = $relation->getQuery(); 257 | 258 | //apply clauses on relation 259 | if (isset($relationBuilder->relationClauses)) { 260 | foreach ($relationBuilder->relationClauses as $clause) { 261 | foreach ($clause as $method => $params) { 262 | $this->applyClauseOnRelation($join, $method, $params, $relatedTableAlias); 263 | } 264 | } 265 | } 266 | 267 | //apply global SoftDeletingScope 268 | foreach ($relationBuilder->scopes as $scope) { 269 | if ($scope instanceof SoftDeletingScope) { 270 | $this->applyClauseOnRelation($join, 'withoutTrashed', [], $relatedTableAlias); 271 | } else { 272 | throw new InvalidRelationGlobalScope(); 273 | } 274 | } 275 | } 276 | 277 | private function applyClauseOnRelation(JoinClause $join, string $method, array $params, string $relatedTableAlias) 278 | { 279 | if (in_array($method, ['where', 'orWhere'])) { 280 | try { 281 | if (is_array($params[0])) { 282 | foreach ($params[0] as $k => $param) { 283 | $params[0][$relatedTableAlias.'.'.$k] = $param; 284 | unset($params[0][$k]); 285 | } 286 | } elseif (is_callable($params[0])) { 287 | throw new InvalidRelationWhere(); 288 | } else { 289 | $params[0] = $relatedTableAlias.'.'.$params[0]; 290 | } 291 | 292 | call_user_func_array([$join, $method], $params); 293 | } catch (\Exception $e) { 294 | throw new InvalidRelationWhere(); 295 | } 296 | } elseif (in_array($method, ['withoutTrashed', 'onlyTrashed', 'withTrashed'])) { 297 | if ('withTrashed' == $method) { 298 | //do nothing 299 | } elseif ('withoutTrashed' == $method) { 300 | call_user_func_array([$join, 'where'], [$relatedTableAlias.'.deleted_at', '=', null]); 301 | } elseif ('onlyTrashed' == $method) { 302 | call_user_func_array([$join, 'where'], [$relatedTableAlias.'.deleted_at', '<>', null]); 303 | } 304 | } else { 305 | throw new InvalidRelationClause(); 306 | } 307 | } 308 | 309 | private function checkAggregateMethod($aggregateMethod) 310 | { 311 | if (!in_array($aggregateMethod, [ 312 | self::AGGREGATE_SUM, 313 | self::AGGREGATE_AVG, 314 | self::AGGREGATE_MAX, 315 | self::AGGREGATE_MIN, 316 | self::AGGREGATE_COUNT, 317 | ])) { 318 | throw new InvalidAggregateMethod(); 319 | } 320 | } 321 | 322 | private function checkDirection($direction) 323 | { 324 | if (!in_array($direction, ['asc', 'desc'], true)) { 325 | throw new InvalidDirection(); 326 | } 327 | } 328 | 329 | //getters and setters 330 | public function isUseTableAlias(): bool 331 | { 332 | return $this->useTableAlias; 333 | } 334 | 335 | public function setUseTableAlias(bool $useTableAlias) 336 | { 337 | $this->useTableAlias = $useTableAlias; 338 | 339 | return $this; 340 | } 341 | 342 | public function isLeftJoin(): bool 343 | { 344 | return $this->leftJoin; 345 | } 346 | 347 | public function setLeftJoin(bool $leftJoin) 348 | { 349 | $this->leftJoin = $leftJoin; 350 | 351 | return $this; 352 | } 353 | 354 | public function isAppendRelationsCount(): bool 355 | { 356 | return $this->appendRelationsCount; 357 | } 358 | 359 | public function setAppendRelationsCount(bool $appendRelationsCount) 360 | { 361 | $this->appendRelationsCount = $appendRelationsCount; 362 | 363 | return $this; 364 | } 365 | 366 | public function getAggregateMethod(): string 367 | { 368 | return $this->aggregateMethod; 369 | } 370 | 371 | public function setAggregateMethod(string $aggregateMethod) 372 | { 373 | $this->checkAggregateMethod($aggregateMethod); 374 | $this->aggregateMethod = $aggregateMethod; 375 | 376 | return $this; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidAggregateMethod.php: -------------------------------------------------------------------------------- 1 | where($column, $operator, $value) and ->where([$column => $value]).'; 8 | } 9 | -------------------------------------------------------------------------------- /src/Relations/BelongsToJoin.php: -------------------------------------------------------------------------------- 1 | useTableAlias)) { 33 | $newEloquentBuilder->setUseTableAlias($this->useTableAlias); 34 | } 35 | 36 | if (isset($this->appendRelationsCount)) { 37 | $newEloquentBuilder->setAppendRelationsCount($this->appendRelationsCount); 38 | } 39 | 40 | if (isset($this->leftJoin)) { 41 | $newEloquentBuilder->setLeftJoin($this->leftJoin); 42 | } 43 | 44 | if (isset($this->aggregateMethod)) { 45 | $newEloquentBuilder->setAggregateMethod($this->aggregateMethod); 46 | } 47 | 48 | return $newEloquentBuilder; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Traits/ExtendRelationsTrait.php: -------------------------------------------------------------------------------- 1 | getQuery() instanceof EloquentJoinBuilder) { 20 | $this->getQuery()->relationClauses[] = [$method => $parameters]; 21 | } 22 | 23 | return parent::__call($method, $parameters); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Models/BaseModel.php: -------------------------------------------------------------------------------- 1 | belongsTo(State::class); 18 | } 19 | 20 | public function zipCodePrimary() 21 | { 22 | return $this->hasOne(ZipCode::class) 23 | ->where('is_primary', '=', 1); 24 | } 25 | 26 | public function sellers() 27 | { 28 | return $this->belongsToMany(Seller::class, 'locations', 'seller_id', 'city_id'); 29 | } 30 | 31 | public function zipCodes() 32 | { 33 | return $this->hasMany(ZipCode::class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Models/Integration.php: -------------------------------------------------------------------------------- 1 | belongsTo(Seller::class, 'id_seller_foreign', 'id_seller_primary'); 18 | } 19 | 20 | public function sellerOwner() 21 | { 22 | return $this->belongsTo(Seller::class, 'id_seller_foreign', 'id_seller_owner'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Models/Key/Seller.php: -------------------------------------------------------------------------------- 1 | hasOne(Location::class, 'id_seller_foreign', 'id_seller_primary'); 18 | } 19 | 20 | public function locations() 21 | { 22 | return $this->hasMany(Location::class, 'id_seller_foreign', 'id_seller_primary'); 23 | } 24 | 25 | public function locationOwner() 26 | { 27 | return $this->hasOne(Location::class, 'id_seller_foreign', 'id_seller_owner'); 28 | } 29 | 30 | public function locationsOwner() 31 | { 32 | return $this->hasMany(Location::class, 'id_seller_foreign', 'id_seller_owner'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Models/Location.php: -------------------------------------------------------------------------------- 1 | belongsTo(Seller::class); 18 | } 19 | 20 | public function city() 21 | { 22 | return $this->belongsTo(City::class); 23 | } 24 | 25 | public function locationAddressPrimary() 26 | { 27 | return $this->hasOne(LocationAddress::class) 28 | ->where('is_primary', '=', 1); 29 | } 30 | 31 | public function integrations() 32 | { 33 | return $this->hasMany(Integration::class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Models/LocationAddress.php: -------------------------------------------------------------------------------- 1 | hasMany(User::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Models/LocationWithGlobalScope.php: -------------------------------------------------------------------------------- 1 | belongsTo(Seller::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Models/OrderItem.php: -------------------------------------------------------------------------------- 1 | belongsTo(Order::class); 18 | } 19 | 20 | public function orderWithTrashed() 21 | { 22 | return $this->belongsTo(Order::class, 'order_id') 23 | ->withTrashed(); 24 | } 25 | 26 | public function orderOnlyTrashed() 27 | { 28 | return $this->belongsTo(Order::class, 'order_id') 29 | ->onlyTrashed(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Models/Seller.php: -------------------------------------------------------------------------------- 1 | hasOne(Location::class) 14 | ->where('is_primary', '=', 0) 15 | ->where('is_secondary', '=', 0); 16 | } 17 | 18 | public function locations() 19 | { 20 | return $this->hasMany(Location::class); 21 | } 22 | 23 | public function locationPrimary() 24 | { 25 | return $this->hasOne(Location::class) 26 | ->where('is_primary', '=', 1); 27 | } 28 | 29 | public function locationPrimaryInvalid() 30 | { 31 | return $this->hasOne(Location::class) 32 | ->where('is_primary', '=', 1) 33 | ->orderBy('is_primary'); 34 | } 35 | 36 | public function locationPrimaryInvalid2() 37 | { 38 | return $this->hasOne(Location::class) 39 | ->where(function ($query) { 40 | return $query->where(['id' => 1]); 41 | }); 42 | } 43 | 44 | public function locationPrimaryInvalid3() 45 | { 46 | return $this->hasOne(LocationWithGlobalScope::class); 47 | } 48 | 49 | public function locationSecondary() 50 | { 51 | return $this->hasOne(Location::class) 52 | ->where('is_secondary', '=', 1); 53 | } 54 | 55 | public function locationPrimaryOrSecondary() 56 | { 57 | return $this->hasOne(Location::class) 58 | ->where('is_primary', '=', 1) 59 | ->orWhere('is_secondary', '=', 1); 60 | } 61 | 62 | public function city() 63 | { 64 | return $this->belongsTo(City::class); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Models/State.php: -------------------------------------------------------------------------------- 1 | hasMany(City::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Models/User.php: -------------------------------------------------------------------------------- 1 | where('test', '=', 'test'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__.'/database/migrations/'); 15 | } 16 | 17 | protected function loadMigrationsFrom($path) 18 | { 19 | $_ENV['type'] = 'sqlite'; //sqlite, mysql, pgsql 20 | 21 | \Artisan::call('migrate', ['--database' => $_ENV['type']]); 22 | 23 | $migrator = $this->app->make('migrator'); 24 | $migrator->run($path); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 1]); 17 | $seller2 = Seller::create(['title' => 2]); 18 | $seller3 = Seller::create(['title' => 3]); 19 | $seller4 = Seller::create(['title' => 4]); 20 | 21 | Location::create(['address' => 1, 'seller_id' => $seller->id]); 22 | Location::create(['address' => 2, 'seller_id' => $seller2->id]); 23 | Location::create(['address' => 3, 'seller_id' => $seller3->id]); 24 | Location::create(['address' => 3, 'seller_id' => $seller3->id]); 25 | 26 | Location::create(['address' => 4, 'seller_id' => $seller3->id, 'is_primary' => 1]); 27 | Location::create(['address' => 5, 'seller_id' => $seller3->id, 'is_secondary' => 1]); 28 | 29 | $order = Order::create(['number' => '1', 'seller_id' => $seller->id]); 30 | $order2 = Order::create(['number' => '2', 'seller_id' => $seller2->id]); 31 | $order3 = Order::create(['number' => '3', 'seller_id' => $seller3->id]); 32 | 33 | OrderItem::create(['name' => '1', 'order_id' => $order->id]); 34 | OrderItem::create(['name' => '2', 'order_id' => $order2->id]); 35 | OrderItem::create(['name' => '3', 'order_id' => $order3->id]); 36 | 37 | $this->startListening(); 38 | } 39 | 40 | protected function startListening() 41 | { 42 | \DB::enableQueryLog(); 43 | } 44 | 45 | protected function fetchLastLog() 46 | { 47 | $log = \DB::getQueryLog(); 48 | 49 | return end($log); 50 | } 51 | 52 | protected function fetchQuery() 53 | { 54 | $query = $this->fetchLastLog()['query']; 55 | $bindings = $this->fetchLastLog()['bindings']; 56 | 57 | foreach ($bindings as $binding) { 58 | $binding = is_string($binding) ? ('"'.$binding.'"') : $binding; 59 | $query = preg_replace('/\?/', $binding, $query, 1); 60 | } 61 | 62 | return $query; 63 | } 64 | 65 | protected function fetchBindings() 66 | { 67 | return $this->fetchLastLog()['bindings']; 68 | } 69 | 70 | protected function getEnvironmentSetUp($app) 71 | { 72 | // Setup default database to use sqlite :memory: 73 | $app['config']->set('database.connections.sqlite', [ 74 | 'driver' => 'sqlite', 75 | 'database' => ':memory:', 76 | 'prefix' => '', 77 | ]); 78 | 79 | $app['config']->set('database.connections.mysql', [ 80 | 'driver' => 'mysql', 81 | 'host' => 'localhost', 82 | 'database' => 'join', 83 | 'username' => 'root', 84 | 'password' => '', 85 | 'charset' => 'utf8', 86 | 'collation' => 'utf8_unicode_ci', 87 | 'strict' => true, 88 | ]); 89 | 90 | $app['config']->set('database.connections.pgsql', [ 91 | 'driver' => 'pgsql', 92 | 'host' => 'localhost', 93 | 'database' => 'join', 94 | 'username' => 'postgres', 95 | 'password' => 'root', 96 | 'charset' => 'utf8', 97 | 'prefix' => '', 98 | 'schema' => 'public', 99 | 'sslmode' => 'prefer', 100 | ]); 101 | 102 | $app['config']->set('database.default', env('type', 'sqlite')); 103 | } 104 | 105 | protected function getPackageProviders($app) 106 | { 107 | return [ServiceProvider::class]; 108 | } 109 | 110 | protected function assertQueryMatches($expected, $actual) 111 | { 112 | $actual = preg_replace('/\s\s+/', ' ', $actual); 113 | $actual = str_replace(['\n', '\r'], '', $actual); 114 | 115 | $expected = preg_replace('/\s\s+/', ' ', $expected); 116 | $expected = str_replace(['\n', '\r'], '', $expected); 117 | $expected = '/'.$expected.'/'; 118 | $expected = preg_quote($expected); 119 | if ('mysql' == $_ENV['type']) { 120 | $expected = str_replace(['"'], '`', $expected); 121 | } 122 | 123 | $this->assertMatchesRegularExpression($expected, $actual); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /tests/Tests/AggregateJoinTest.php: -------------------------------------------------------------------------------- 1 | orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_SUM) 22 | ->get(); 23 | 24 | $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_SUM, $this->queryTest); 25 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 26 | } 27 | 28 | public function testSum() 29 | { 30 | Order::joinRelations('seller') 31 | ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_AVG) 32 | ->get(); 33 | 34 | $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_AVG, $this->queryTest); 35 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 36 | } 37 | 38 | public function testMax() 39 | { 40 | Order::joinRelations('seller') 41 | ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_MAX) 42 | ->get(); 43 | 44 | $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_MAX, $this->queryTest); 45 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 46 | } 47 | 48 | public function testMin() 49 | { 50 | Order::joinRelations('seller') 51 | ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_MIN) 52 | ->get(); 53 | 54 | $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_MIN, $this->queryTest); 55 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 56 | } 57 | 58 | public function testCount() 59 | { 60 | Order::joinRelations('seller') 61 | ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_COUNT) 62 | ->get(); 63 | 64 | $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_COUNT, $this->queryTest); 65 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Tests/AppendRelationsCountTest.php: -------------------------------------------------------------------------------- 1 | joinRelations('seller.locationPrimary.locationAddressPrimary')->get(); 13 | 14 | $queryTest = 'select COUNT(sellers.id) as sellers_count, COUNT(locations.id) as sellers_locations_count, COUNT(location_addresses.id) as sellers_locations_location_addresses_count, orders.* 15 | from "orders" 16 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 17 | left join "locations" on "locations"."seller_id" = "sellers"."id" 18 | and "locations"."is_primary" = 1 19 | and "locations"."deleted_at" is null 20 | left join "location_addresses" on "location_addresses"."location_id" = "locations"."id" 21 | and "location_addresses"."is_primary" = 1 22 | and "location_addresses"."deleted_at" is null 23 | where "orders"."deleted_at" is null 24 | group by "orders"."id"'; 25 | 26 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/JoinRelationsTest.php: -------------------------------------------------------------------------------- 1 | get(); 14 | 15 | $queryTest = 'select orders.* 16 | from "orders" 17 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 18 | where "orders"."deleted_at" is null 19 | group by "orders"."id"'; 20 | 21 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/OrWhereInTest.php: -------------------------------------------------------------------------------- 1 | whereInJoin('seller.id', [1, 2]) 14 | ->orWhereInJoin('seller.id', [3, 4]) 15 | ->get(); 16 | 17 | $queryTest = 'select orders.* 18 | from "orders" 19 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 20 | where ("sellers"."id" in (1, 2) 21 | or 22 | "sellers"."id" in (3, 4)) 23 | and "orders"."deleted_at" is null 24 | group by "orders"."id"'; 25 | 26 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/OrWhereNotInTest.php: -------------------------------------------------------------------------------- 1 | whereInJoin('seller.id', [1, 2]) 14 | ->orWhereNotInJoin('seller.id', [3, 4]) 15 | ->get(); 16 | 17 | $queryTest = 'select orders.* 18 | from "orders" 19 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 20 | where ("sellers"."id" in (1, 2) 21 | or 22 | "sellers"."id" not in (3, 4)) 23 | and "orders"."deleted_at" is null 24 | group by "orders"."id"'; 25 | 26 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/OrWhereTest.php: -------------------------------------------------------------------------------- 1 | whereJoin('seller.id', '=', 1) 14 | ->orWhereJoin('seller.id', '=', 2) 15 | ->get(); 16 | 17 | $queryTest = 'select orders.* 18 | from "orders" 19 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 20 | where ("sellers"."id" = 1 or "sellers"."id" = 2) 21 | and "orders"."deleted_at" is null 22 | group by "orders"."id"'; 23 | 24 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/OrderByTest.php: -------------------------------------------------------------------------------- 1 | orderByJoin('seller.id', 'asc') 14 | ->get(); 15 | 16 | $queryTest = 'select orders.*, MAX("sellers"."id") as sort 17 | from "orders" 18 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 19 | where "orders"."deleted_at" is null 20 | group by "orders"."id" 21 | order by sort asc'; 22 | 23 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 24 | } 25 | 26 | public function testOrderByMultiple() 27 | { 28 | Order::joinRelations('seller') 29 | ->orderByJoin('seller.id', 'asc') 30 | ->orderByJoin('seller.title', 'desc') 31 | ->get(); 32 | 33 | $queryTest = 'select orders.*, MAX("sellers"."id") as sort, MAX("sellers"."title") as sort2 34 | from "orders" 35 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 36 | where "orders"."deleted_at" is null 37 | group by "orders"."id" 38 | order by sort asc, sort2 desc'; 39 | 40 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/WhereInTest.php: -------------------------------------------------------------------------------- 1 | whereInJoin('seller.id', [1, 2]) 14 | ->get(); 15 | 16 | $queryTest = 'select orders.* 17 | from "orders" 18 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 19 | where "sellers"."id" in (1, 2) 20 | and "orders"."deleted_at" is null 21 | group by "orders"."id"'; 22 | 23 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/WhereNotInTest.php: -------------------------------------------------------------------------------- 1 | whereNotInJoin('seller.id', [1, 2]) 14 | ->get(); 15 | 16 | $queryTest = 'select orders.* 17 | from "orders" 18 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 19 | where "sellers"."id" not in (1, 2) 20 | and "orders"."deleted_at" is null 21 | group by "orders"."id"'; 22 | 23 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Tests/Clauses/WhereTest.php: -------------------------------------------------------------------------------- 1 | whereJoin('seller.id', '=', 1) 14 | ->get(); 15 | 16 | $queryTest = 'select orders.* 17 | from "orders" 18 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 19 | where "sellers"."id" = 1 20 | and "orders"."deleted_at" is null 21 | group by "orders"."id"'; 22 | 23 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Tests/ClosureOnRelationTest.php: -------------------------------------------------------------------------------- 1 | 0', 'is_secondary' => 0] 13 | $items = Seller::orderByJoin('location.id', 'desc')->get(); 14 | $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" 15 | left join "locations" 16 | on "locations"."seller_id" = "sellers"."id" 17 | and "locations"."is_primary" = 0 18 | and "locations"."is_secondary" = 0 19 | and "locations"."deleted_at" is null 20 | group by "sellers"."id" 21 | order by sort desc'; 22 | 23 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 24 | 25 | //locationPrimary have one where ['is_primary => 1'] 26 | $items = Seller::orderByJoin('locationPrimary.id', 'desc')->get(); 27 | $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" 28 | left join "locations" 29 | on "locations"."seller_id" = "sellers"."id" 30 | and "locations"."is_primary" = 1 31 | and "locations"."deleted_at" is null 32 | group by "sellers"."id" 33 | order by sort desc'; 34 | 35 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 36 | 37 | //locationPrimary have one where ['is_secondary => 1'] 38 | $items = Seller::orderByJoin('locationSecondary.id', 'desc')->get(); 39 | $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" 40 | left join "locations" 41 | on "locations"."seller_id" = "sellers"."id" 42 | and "locations"."is_secondary" = 1 43 | and "locations"."deleted_at" is null 44 | group by "sellers"."id" 45 | order by sort desc'; 46 | 47 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 48 | 49 | //locationPrimary have one where ['is_primary => 1'] and one orWhere ['is_secondary => 1'] 50 | $items = Seller::orderByJoin('locationPrimaryOrSecondary.id', 'desc')->get(); 51 | $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" 52 | left join "locations" 53 | on "locations"."seller_id" = "sellers"."id" 54 | and "locations"."is_primary" = 1 55 | or "locations"."is_secondary" = 1 56 | and "locations"."deleted_at" is null 57 | group by "sellers"."id" 58 | order by sort desc'; 59 | 60 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 61 | } 62 | 63 | public function testWhereOnRelationWithoutOrderByJoin() 64 | { 65 | $seller = Seller::find(1); 66 | 67 | $seller->locationPrimary; 68 | $queryTest = 'select * from "locations" 69 | where "locations"."seller_id" = 1 70 | and "locations"."seller_id" is not null 71 | and "is_primary" = 1 72 | and "locations"."deleted_at" is null 73 | limit 1'; 74 | 75 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 76 | 77 | $seller->locationPrimary()->where(['is_secondary' => 1])->get(); 78 | $queryTest = 'select * from "locations" 79 | where "locations"."seller_id" = 1 80 | and "locations"."seller_id" is not null 81 | and "is_primary" = 1 82 | and ("is_secondary" = 1) 83 | and "locations"."deleted_at" is null'; 84 | 85 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Tests/ClosureTest.php: -------------------------------------------------------------------------------- 1 | orWhereJoin('order.id', '=', 1) 15 | ->orWhereJoin('order.id', '=', 2); 16 | })->get(); 17 | 18 | $queryTest = 'select order_items.* 19 | from "order_items" 20 | left join "orders" on "orders"."id" = "order_items"."order_id" 21 | and "orders"."deleted_at" is null 22 | where ("orders"."id" = 1 or "orders"."id" = 2) 23 | and "order_items"."deleted_at" is null'; 24 | 25 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 26 | } 27 | 28 | public function testNestTwo() 29 | { 30 | OrderItem::where(function ($query) { 31 | $query 32 | ->orWhereJoin('order.id', '=', 1) 33 | ->orWhereJoin('order.id', '=', 2) 34 | ->where(function ($query) { 35 | $query->orWhereJoin('order.seller.locationPrimary.id', '=', 3); 36 | }); 37 | })->get(); 38 | 39 | $queryTest = 'select order_items.* 40 | from "order_items" 41 | left join "orders" on "orders"."id" = "order_items"."order_id" 42 | and "orders"."deleted_at" is null 43 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 44 | left join "locations" on "locations"."seller_id" = "sellers"."id" 45 | and "locations"."is_primary" = 1 46 | and "locations"."deleted_at" is null 47 | where ("orders"."id" = 1 or "orders"."id" = 2 48 | and ("locations"."id" = 3)) 49 | and "order_items"."deleted_at" is null'; 50 | 51 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Tests/ExceptionTest.php: -------------------------------------------------------------------------------- 1 | get(); 21 | } catch (InvalidRelation $e) { 22 | $this->assertEquals((new InvalidRelation())->message, $e->getMessage()); 23 | 24 | return; 25 | } 26 | 27 | $this->assertTrue(false); 28 | } 29 | 30 | public function testInvalidRelationWhere() 31 | { 32 | try { 33 | Seller::whereJoin('locationPrimaryInvalid2.name', '=', 'test')->get(); 34 | } catch (InvalidRelationWhere $e) { 35 | $this->assertEquals((new InvalidRelationWhere())->message, $e->getMessage()); 36 | 37 | return; 38 | } 39 | 40 | $this->assertTrue(false); 41 | } 42 | 43 | public function testInvalidRelationClause() 44 | { 45 | try { 46 | Seller::whereJoin('locationPrimaryInvalid.name', '=', 'test')->get(); 47 | } catch (InvalidRelationClause $e) { 48 | $this->assertEquals((new InvalidRelationClause())->message, $e->getMessage()); 49 | 50 | return; 51 | } 52 | 53 | $this->assertTrue(false); 54 | } 55 | 56 | public function testInvalidRelationGlobalScope() 57 | { 58 | try { 59 | Seller::whereJoin('locationPrimaryInvalid3.id', '=', 'test')->get(); 60 | } catch (InvalidRelationGlobalScope $e) { 61 | $this->assertEquals((new InvalidRelationGlobalScope())->message, $e->getMessage()); 62 | 63 | return; 64 | } 65 | 66 | $this->assertTrue(false); 67 | } 68 | 69 | public function testInvalidAggregateMethod() 70 | { 71 | try { 72 | Seller::orderByJoin('locationPrimary.id', 'asc', 'wrong')->get(); 73 | } catch (InvalidAggregateMethod $e) { 74 | $this->assertEquals((new InvalidAggregateMethod())->message, $e->getMessage()); 75 | 76 | return; 77 | } 78 | 79 | $this->assertTrue(false); 80 | } 81 | 82 | public function testOrderByInvalidDirection() 83 | { 84 | $this->expectException(InvalidDirection::class); 85 | Seller::orderByJoin('locationPrimary.id', ';DROP TABLE orders;--', 'test')->get(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Tests/JoinTypeTest.php: -------------------------------------------------------------------------------- 1 | whereJoin('city.name', '=', 'test')->get(); 14 | 15 | $queryTest = 'select sellers.* 16 | from "sellers" 17 | left join "cities" 18 | on "cities"."id" = "sellers"."city_id" 19 | and "cities"."deleted_at" is null 20 | where "cities"."name" = "test"'; 21 | 22 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 23 | } 24 | 25 | public function testInnerJoin() 26 | { 27 | Seller::setLeftJoin(false)->whereJoin('city.name', '=', 'test')->get(); 28 | 29 | $queryTest = 'select sellers.* 30 | from "sellers" 31 | inner join "cities" 32 | on "cities"."id" = "sellers"."city_id" 33 | and "cities"."deleted_at" is null 34 | where "cities"."name" = "test"'; 35 | 36 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 37 | } 38 | 39 | public function testMixedJoin() 40 | { 41 | Order::joinRelations('seller', true)->joinRelations('seller.city', false)->joinRelations('seller.city.state', true)->get(); 42 | 43 | $queryTest = 'select orders.* 44 | from "orders" 45 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 46 | inner join "cities" on "cities"."id" = "sellers"."city_id" 47 | and "cities"."deleted_at" is null 48 | left join "states" on "states"."id" = "cities"."state_id" 49 | and "states"."deleted_at" is null 50 | where "orders"."deleted_at" is null 51 | group by "orders"."id"'; 52 | 53 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Tests/KeysOwnerTest.php: -------------------------------------------------------------------------------- 1 | get(); 15 | 16 | $queryTest = 'select key_orders.* 17 | from "key_orders" 18 | left join "key_sellers" on "key_sellers"."id_seller_owner" = "key_orders"."id_seller_foreign" 19 | group by "key_orders"."id_order_primary"'; 20 | 21 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 22 | } 23 | 24 | public function testHasOne() 25 | { 26 | Seller::joinRelations('locationOwner') 27 | ->get(); 28 | 29 | $queryTest = 'select key_sellers.* 30 | from "key_sellers" 31 | left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_owner" 32 | group by "key_sellers"."id_seller_primary"'; 33 | 34 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 35 | } 36 | 37 | public function testHasMany() 38 | { 39 | Seller::joinRelations('locationsOwner') 40 | ->get(); 41 | 42 | $queryTest = 'select key_sellers.* 43 | from "key_sellers" 44 | left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_owner" 45 | group by "key_sellers"."id_seller_primary"'; 46 | 47 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Tests/KeysTest.php: -------------------------------------------------------------------------------- 1 | get(); 15 | 16 | $queryTest = 'select key_orders.* 17 | from "key_orders" 18 | left join "key_sellers" on "key_sellers"."id_seller_primary" = "key_orders"."id_seller_foreign" 19 | group by "key_orders"."id_order_primary"'; 20 | 21 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 22 | } 23 | 24 | public function testHasOne() 25 | { 26 | Seller::joinRelations('location') 27 | ->get(); 28 | 29 | $queryTest = 'select key_sellers.* 30 | from "key_sellers" 31 | left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_primary" 32 | group by "key_sellers"."id_seller_primary"'; 33 | 34 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 35 | } 36 | 37 | public function testHasMany() 38 | { 39 | Seller::joinRelations('locations') 40 | ->get(); 41 | 42 | $queryTest = 'select key_sellers.* 43 | from "key_sellers" 44 | left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_primary" 45 | group by "key_sellers"."id_seller_primary"'; 46 | 47 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Tests/OptionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(false, $city->newModelQuery()->isUseTableAlias()); 15 | $city->useTableAlias = true; 16 | $this->assertEquals(true, $city->newModelQuery()->isUseTableAlias()); 17 | } 18 | 19 | public function testAppendRelationsCount() 20 | { 21 | $city = new City(); 22 | $this->assertEquals(false, $city->newModelQuery()->isAppendRelationsCount()); 23 | $city->appendRelationsCount = true; 24 | $this->assertEquals(true, $city->newModelQuery()->isAppendRelationsCount()); 25 | } 26 | 27 | public function testLeftJoin() 28 | { 29 | $city = new City(); 30 | $this->assertEquals(true, $city->newModelQuery()->isLeftJoin()); 31 | $city->leftJoin = false; 32 | $this->assertEquals(false, $city->newModelQuery()->isLeftJoin()); 33 | } 34 | 35 | public function testAggregateMethod() 36 | { 37 | $city = new City(); 38 | $this->assertEquals(EloquentJoinBuilder::AGGREGATE_MAX, $city->newModelQuery()->getAggregateMethod()); 39 | $city->aggregateMethod = EloquentJoinBuilder::AGGREGATE_MIN; 40 | $this->assertEquals(EloquentJoinBuilder::AGGREGATE_MIN, $city->newModelQuery()->getAggregateMethod()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Tests/Relations/BelongsToTest.php: -------------------------------------------------------------------------------- 1 | get(); 13 | 14 | $queryTest = 'select orders.* from "orders" 15 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 16 | where "orders"."deleted_at" is null 17 | group by "orders"."id"'; 18 | 19 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 20 | } 21 | 22 | public function testBelongsToHasOne() 23 | { 24 | Order::joinRelations('seller.locationPrimary')->get(); 25 | 26 | $queryTest = 'select orders.* from "orders" 27 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 28 | left join "locations" on "locations"."seller_id" = "sellers"."id" 29 | and "locations"."is_primary" = 1 30 | and "locations"."deleted_at" is null 31 | where "orders"."deleted_at" is null 32 | group by "orders"."id"'; 33 | 34 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 35 | } 36 | 37 | public function testBelongsToHasMany() 38 | { 39 | Order::joinRelations('seller.locations')->get(); 40 | 41 | $queryTest = 'select orders.* from "orders" 42 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 43 | left join "locations" on "locations"."seller_id" = "sellers"."id" 44 | and "locations"."deleted_at" is null 45 | where "orders"."deleted_at" is null 46 | group by "orders"."id"'; 47 | 48 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 49 | } 50 | 51 | public function testBelongsToHasOneHasMany() 52 | { 53 | Order::joinRelations('seller.locationPrimary.integrations')->get(); 54 | 55 | $queryTest = 'select orders.* from "orders" 56 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 57 | left join "locations" on "locations"."seller_id" = "sellers"."id" 58 | and "locations"."is_primary" = 1 and "locations"."deleted_at" is null 59 | left join "integrations" on "integrations"."location_id" = "locations"."id" 60 | and "integrations"."deleted_at" is null 61 | where "orders"."deleted_at" is null 62 | group by "orders"."id"'; 63 | 64 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 65 | } 66 | 67 | public function testBelongsToHasManyHasOne() 68 | { 69 | Order::joinRelations('seller.locationPrimary.locationAddressPrimary')->get(); 70 | 71 | $queryTest = 'select orders.* from "orders" 72 | left join "sellers" on "sellers"."id" = "orders"."seller_id" 73 | left join "locations" on "locations"."seller_id" = "sellers"."id" 74 | and "locations"."is_primary" = 1 75 | and "locations"."deleted_at" is null 76 | left join "location_addresses" on "location_addresses"."location_id" = "locations"."id" 77 | and "location_addresses"."is_primary" = 1 78 | and "location_addresses"."deleted_at" is null 79 | where "orders"."deleted_at" is null 80 | group by "orders"."id"'; 81 | 82 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Tests/Relations/HasManyTest.php: -------------------------------------------------------------------------------- 1 | get(); 13 | 14 | $queryTest = 'select sellers.* 15 | from "sellers" 16 | left join "locations" on "locations"."seller_id" = "sellers"."id" 17 | and "locations"."deleted_at" is null 18 | group by "sellers"."id"'; 19 | 20 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 21 | } 22 | 23 | public function testHasManyHasOne() 24 | { 25 | Seller::joinRelations('locations.city')->get(); 26 | 27 | $queryTest = 'select sellers.* 28 | from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" 29 | and "locations"."deleted_at" is null 30 | left join "cities" on "cities"."id" = "locations"."city_id" 31 | and "cities"."deleted_at" is null 32 | group by "sellers"."id"'; 33 | 34 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 35 | } 36 | 37 | public function testHasManyBelongsTo() 38 | { 39 | Seller::joinRelations('locations.integrations')->get(); 40 | 41 | $queryTest = 'select sellers.* 42 | from "sellers" 43 | left join "locations" on "locations"."seller_id" = "sellers"."id" 44 | and "locations"."deleted_at" is null 45 | left join "integrations" on "integrations"."location_id" = "locations"."id" 46 | and "integrations"."deleted_at" is null 47 | group by "sellers"."id"'; 48 | 49 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Tests/Relations/HasOneTest.php: -------------------------------------------------------------------------------- 1 | get(); 13 | 14 | $queryTest = 'select sellers.* 15 | from "sellers" 16 | left join "locations" on "locations"."seller_id" = "sellers"."id" 17 | and "locations"."is_primary" = 0 18 | and "locations"."is_secondary" = 0 19 | and "locations"."deleted_at" is null 20 | group by "sellers"."id"'; 21 | 22 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 23 | } 24 | 25 | public function testHasOneBelongsTo() 26 | { 27 | Seller::joinRelations('location.city')->get(); 28 | 29 | $queryTest = 'select sellers.* 30 | from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" 31 | and "locations"."is_primary" = 0 32 | and "locations"."is_secondary" = 0 33 | and "locations"."deleted_at" is null 34 | left join "cities" on "cities"."id" = "locations"."city_id" 35 | and "cities"."deleted_at" is null 36 | group by "sellers"."id"'; 37 | 38 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 39 | } 40 | 41 | public function testHasOneHasMany() 42 | { 43 | Seller::joinRelations('location.integrations')->get(); 44 | 45 | $queryTest = 'select sellers.* 46 | from "sellers" 47 | left join "locations" on "locations"."seller_id" = "sellers"."id" 48 | and "locations"."is_primary" = 0 49 | and "locations"."is_secondary" = 0 50 | and "locations"."deleted_at" is null 51 | left join "integrations" on "integrations"."location_id" = "locations"."id" 52 | and "integrations"."deleted_at" is null 53 | group by "sellers"."id"'; 54 | 55 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Tests/SoftDeleteTest.php: -------------------------------------------------------------------------------- 1 | get(); 13 | $queryTest = 'select * 14 | from "order_items" 15 | where "order_items"."deleted_at" is null 16 | order by "order_items"."name" asc'; 17 | 18 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 19 | } 20 | 21 | public function testNotRelatedWithoutTrashedExplicit() 22 | { 23 | OrderItem::orderByJoin('name')->withoutTrashed()->get(); 24 | $queryTest = 'select * 25 | from "order_items" 26 | where "order_items"."deleted_at" is null 27 | order by "order_items"."name" asc'; 28 | 29 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 30 | } 31 | 32 | public function testNotRelatedOnlyTrashedExplicit() 33 | { 34 | OrderItem::orderByJoin('name')->onlyTrashed()->get(); 35 | $queryTest = 'select * 36 | from "order_items" 37 | where "order_items"."deleted_at" is not null 38 | order by "order_items"."name" asc'; 39 | 40 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 41 | } 42 | 43 | public function testNotRelatedWithTrashedExplicit() 44 | { 45 | OrderItem::orderByJoin('name')->withTrashed()->get(); 46 | $queryTest = 'select * 47 | from "order_items" 48 | order by "order_items"."name" asc'; 49 | 50 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 51 | } 52 | 53 | public function testRelatedWithoutTrashedDefault() 54 | { 55 | OrderItem::orderByJoin('order.number')->get(); 56 | $queryTest = 'select order_items.*, MAX("orders"."number") as sort 57 | from "order_items" left join "orders" 58 | on "orders"."id" = "order_items"."order_id" 59 | and "orders"."deleted_at" is null 60 | where "order_items"."deleted_at" is null 61 | group by "order_items"."id" 62 | order by sort asc'; 63 | 64 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 65 | } 66 | 67 | public function testRelatedWithoutTrashedExplicit() 68 | { 69 | OrderItem::orderByJoin('order.number')->withoutTrashed()->get(); 70 | $queryTest = 'select order_items.*, MAX("orders"."number") as sort 71 | from "order_items" 72 | left join "orders" 73 | on "orders"."id" = "order_items"."order_id" 74 | and "orders"."deleted_at" is null 75 | where "order_items"."deleted_at" is null 76 | group by "order_items"."id" 77 | order by sort asc'; 78 | 79 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 80 | } 81 | 82 | public function testRelatedOnlyTrashedExplicit() 83 | { 84 | OrderItem::orderByJoin('order.number')->onlyTrashed()->get(); 85 | $queryTest = 'select order_items.*, MAX("orders"."number") as sort 86 | from "order_items" 87 | left join "orders" 88 | on "orders"."id" = "order_items"."order_id" 89 | and "orders"."deleted_at" is null 90 | where "order_items"."deleted_at" is not null 91 | group by "order_items"."id" 92 | order by sort asc'; 93 | 94 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 95 | } 96 | 97 | public function testRelatedWithTrashedExplicit() 98 | { 99 | OrderItem::orderByJoin('order.number')->withTrashed()->get(); 100 | $queryTest = 'select order_items.*, MAX("orders"."number") as sort 101 | from "order_items" 102 | left join "orders" 103 | on "orders"."id" = "order_items"."order_id" 104 | and "orders"."deleted_at" is null 105 | group by "order_items"."id" 106 | order by sort asc'; 107 | 108 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 109 | } 110 | 111 | public function testRelatedWithTrashedOnRelation() 112 | { 113 | OrderItem::orderByJoin('orderWithTrashed.number')->get(); 114 | $queryTest = 'select order_items.*, MAX("orders"."number") as sort 115 | from "order_items" 116 | left join "orders" 117 | on "orders"."id" = "order_items"."order_id" 118 | where "order_items"."deleted_at" is null 119 | group by "order_items"."id" 120 | order by sort asc'; 121 | 122 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 123 | } 124 | 125 | public function testRelatedOnlyTrashedOnRelation() 126 | { 127 | OrderItem::orderByJoin('orderOnlyTrashed.number')->get(); 128 | $queryTest = 'select order_items.*, MAX("orders"."number") as sort 129 | from "order_items" 130 | left join "orders" 131 | on "orders"."id" = "order_items"."order_id" 132 | and "orders"."deleted_at" is not null 133 | where "order_items"."deleted_at" is null 134 | group by "order_items"."id" 135 | order by sort asc'; 136 | 137 | $this->assertQueryMatches($queryTest, $this->fetchQuery()); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /tests/database/migrations/2017_11_04_163552_create_database.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('name')->nullable(); 19 | 20 | $table->timestamps(); 21 | $table->softDeletes(); 22 | }); 23 | 24 | Schema::create('cities', function (Blueprint $table) { 25 | $table->increments('id'); 26 | $table->string('name')->nullable(); 27 | $table->unsignedInteger('state_id')->nullable(); 28 | 29 | $table->foreign('state_id')->references('id')->on('states'); 30 | 31 | $table->timestamps(); 32 | $table->softDeletes(); 33 | }); 34 | 35 | Schema::create('sellers', function (Blueprint $table) { 36 | $table->increments('id'); 37 | $table->string('title')->nullable(); 38 | $table->unsignedInteger('city_id')->nullable(); 39 | 40 | $table->foreign('city_id')->references('id')->on('cities'); 41 | 42 | $table->timestamps(); 43 | $table->softDeletes(); 44 | }); 45 | 46 | Schema::create('orders', function (Blueprint $table) { 47 | $table->increments('id'); 48 | $table->string('number')->nullable(); 49 | $table->unsignedInteger('seller_id')->nullable(); 50 | 51 | $table->foreign('seller_id')->references('id')->on('sellers'); 52 | 53 | $table->timestamps(); 54 | $table->softDeletes(); 55 | }); 56 | 57 | Schema::create('order_items', function (Blueprint $table) { 58 | $table->increments('id'); 59 | $table->string('name'); 60 | $table->unsignedInteger('order_id')->nullable(); 61 | 62 | $table->foreign('order_id')->references('id')->on('orders'); 63 | 64 | $table->timestamps(); 65 | $table->softDeletes(); 66 | }); 67 | 68 | Schema::create('locations', function (Blueprint $table) { 69 | $table->increments('id'); 70 | $table->string('address')->nullable(); 71 | $table->boolean('is_primary')->default(0); 72 | $table->boolean('is_secondary')->default(0); 73 | $table->unsignedInteger('seller_id')->nullable(); 74 | $table->unsignedInteger('city_id')->nullable(); 75 | 76 | $table->foreign('seller_id')->references('id')->on('sellers'); 77 | $table->foreign('city_id')->references('id')->on('cities'); 78 | 79 | $table->timestamps(); 80 | $table->softDeletes(); 81 | }); 82 | 83 | Schema::create('zip_codes', function (Blueprint $table) { 84 | $table->increments('id'); 85 | $table->string('name')->nullable(); 86 | $table->boolean('is_primary')->default(0); 87 | $table->unsignedInteger('city_id')->nullable(); 88 | 89 | $table->foreign('city_id')->references('id')->on('cities'); 90 | 91 | $table->timestamps(); 92 | $table->softDeletes(); 93 | }); 94 | 95 | Schema::create('location_addresses', function (Blueprint $table) { 96 | $table->increments('id'); 97 | $table->string('name')->nullable(); 98 | $table->boolean('is_primary')->default(0); 99 | $table->unsignedInteger('location_id')->nullable(); 100 | 101 | $table->foreign('location_id')->references('id')->on('locations'); 102 | 103 | $table->timestamps(); 104 | $table->softDeletes(); 105 | }); 106 | 107 | Schema::create('users', function (Blueprint $table) { 108 | $table->increments('id'); 109 | $table->string('name')->nullable(); 110 | $table->unsignedInteger('location_address_id')->nullable(); 111 | 112 | $table->foreign('location_address_id')->references('id')->on('location_addresses'); 113 | 114 | $table->timestamps(); 115 | $table->softDeletes(); 116 | }); 117 | 118 | Schema::create('integrations', function (Blueprint $table) { 119 | $table->increments('id'); 120 | $table->string('name')->nullable(); 121 | $table->unsignedInteger('location_id')->nullable(); 122 | 123 | $table->foreign('location_id')->references('id')->on('locations'); 124 | 125 | $table->timestamps(); 126 | $table->softDeletes(); 127 | }); 128 | 129 | //for key tests 130 | Schema::create('key_orders', function (Blueprint $table) { 131 | $table->increments('id_order_primary'); 132 | $table->unsignedInteger('id_order_owner')->nullable(); 133 | 134 | $table->string('number')->nullable(); 135 | 136 | $table->unsignedInteger('id_seller_foreign')->nullable(); 137 | $table->foreign('id_seller_foreign')->references('id')->on('sellers'); 138 | }); 139 | 140 | Schema::create('key_sellers', function (Blueprint $table) { 141 | $table->increments('id_seller_primary'); 142 | $table->unsignedInteger('id_seller_owner')->nullable(); 143 | 144 | $table->string('title')->nullable(); 145 | }); 146 | 147 | Schema::create('key_locations', function (Blueprint $table) { 148 | $table->increments('id_location_primary'); 149 | $table->unsignedInteger('id_location_owner')->nullable(); 150 | 151 | $table->string('address')->nullable(); 152 | 153 | $table->unsignedInteger('id_seller_foreign')->nullable(); 154 | $table->foreign('id_seller_foreign')->references('id')->on('sellers'); 155 | }); 156 | } 157 | 158 | /** 159 | * Reverse the migrations. 160 | */ 161 | public function down() 162 | { 163 | Schema::drop('users'); 164 | Schema::drop('sellers'); 165 | Schema::drop('order_items'); 166 | Schema::drop('locations'); 167 | Schema::drop('cities'); 168 | Schema::drop('zip_codes'); 169 | Schema::drop('states'); 170 | Schema::drop('location_addresses'); 171 | Schema::drop('integrations'); 172 | Schema::drop('orders'); 173 | 174 | //for key tests 175 | Schema::drop('key_orders'); 176 | Schema::drop('key_sellers'); 177 | Schema::drop('key_locations'); 178 | } 179 | } 180 | --------------------------------------------------------------------------------