Repository: fico7489/laravel-eloquent-join Branch: master Commit: 847096fac908 Files: 60 Total size: 90.0 KB Directory structure: gitextract_4l8jykqq/ ├── .github/ │ └── workflows/ │ └── test.yml ├── .gitignore ├── .php_cs ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: pull_request: push: branches: - master jobs: test: runs-on: ubuntu-latest strategy: matrix: php_version: [7.4, 8.2] laravel_version: [8.*, 10.*, 11.*] steps: - name: Checkout commit uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php_version }} - name: Validate composer.json run: composer validate - name: Run composer install run: composer install --no-interaction --no-suggest - name: Run find-and-replace to replace * with 0 uses: mad9000/actions-find-and-replace-string@1 id: laravel_version_cleaned with: source: ${{ matrix.laravel_version }} find: '*' replace: '0' - name: Install Laravel run: composer update --no-interaction illuminate/database:^${{ steps.laravel_version_cleaned.outputs.value }} - name: Run PHPUnit run: ./vendor/bin/phpunit ================================================ FILE: .gitignore ================================================ /vendor /.idea composer.lock .php_cs.cache .phpunit.result.cache ================================================ FILE: .php_cs ================================================ in([ __DIR__ .'/src', __DIR__ .'/tests', ]); ; /* * Do the magic */ return Config::create() ->setUsingCache(false) ->setRules([ '@PSR2' => true, '@Symfony' => true, ]) ->setFinder($finder) ; ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Filip Horvat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![Tests status](https://github.com/fico7489/laravel-eloquent-join/workflows/Test/badge.svg) # Laravel Eloquent Join This package introduces the join magic for eloquent models and relations. ## Introduction Eloquent is a powerful ORM but its join capabilities are very poor. #### First Eloquent Problem (sorting) 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: ``` $posts = Post::select('posts.*') ->join('categories', 'categories.id', '=', 'posts.category_id') ->groupBy('posts.id') ->where('categories.deleted_at', '=', null) ->orderBy('categories.name'); if(request()->get('date')){ $posts->where('posts.date', $date) } $posts = $posts->get(); ``` 1.The first problem is that you need to worry about select. ``` ->select('posts.*') ``` Reason : without **select** id from the category can be selected and hydrated into the Post model. 2.The second problem is that you need to worry about **groupBy**. ->groupBy('posts.id'); Reason : if the relation is HasOne and there are more than one categories for the post, the query will return more rows for categories. 3.The third problem is that you need to change all other where clauses from : ``` ->where('date', $date) ``` to ``` ->where('posts.date', $date) ``` 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. 4.The fourth problem is that you are using table names(not models) and this is also bad and awkward. ``` ->where('posts.date', $date) ``` 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 : ``` ->where('categories.deleted_at', '=', null) ``` This package will take care of all above problems for you. 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. #### Second Eloquent Problem (subqueries) With laravel you can perform where on the relationship attribute but laravel will generate subqueries which are more slower than joins. With this package you will be available to perform where on the relationship with joins in an elegant way. ## Requirements | Laravel Version | Package Tag | Supported | Development Branch |-----------------|-------------|-----------| -----------| | >= 5.5.0 | 4.* | yes | master | < 5.5.0 | - | no | - Package is also tested for SQLite, MySql and PostgreSql ## Installation & setup 1.Install package with composer ``` composer require fico7489/laravel-eloquent-join ``` With this statement, a composer will install highest available package version for your current laravel version. 2.Use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoinTrait trait in your base model or only in particular models. ``` ... use Fico7489\Laravel\EloquentJoin\Traits\EloquentJoin; use Illuminate\Database\Eloquent\Model; abstract class BaseModel extends Model { use EloquentJoin; ... ``` 3.IMPORTANT For **MySql** make sure that **strict** configuration is set to **false** config/database.php ``` 'mysql' => [ ... 'strict' => false, ... ``` and that's it, you are ready to go. ## Options Options can be set in the model : ``` class Seller extends BaseModel { protected $useTableAlias = false; protected $appendRelationsCount = false; protected $leftJoin = false; protected $aggregateMethod = 'MAX'; ``` or on query : ``` Order::setUseTableAlias(true)->get(); Order::setAppendRelationsCount(true)->get(); Order::setLeftJoin(true)->get(); Order::setAggregateMethod(true)->get(); ``` #### **useTableAlias** Should we use an alias for joined tables (default = false) With **true** query will look like this : ``` select "sellers".* from "sellers" left join "locations" as "5b5c093d2e00f" ... ``` With **false** query will look like this : ``` select "sellers".* from "sellers" left join "locations" ... ``` Alias is a randomly generated string. #### **appendRelationsCount** Should we automatically append relation count field to results (default = false) With **true** query will look like this : ``` select "sellers".*, count(locations.id) AS locations_count from "sellers" left join "locations" as "5b5c093d2e00f" ... ``` Each **relation** is glued with an underscore and at the end **_count** prefix is added. For example for ->joinRelations('seller.locations') field would be __seller_locations_count__ #### **leftJoin** Should we use **inner join** or **left join** (default = true) ``` select "sellers".* from "sellers" inner join "locations" ... ``` vs ``` select "sellers".* from "sellers" left join "locations" ... ``` #### **aggregateMethod** Which aggregate method to use for ordering (default = 'MAX'). 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. ``` select "sellers".*, MAX("locations" ."number") AS sort from "sellers" left join "locations" group by "locations" ."id" order by sort ... ``` Options are : **SUM**, **AVG**, **MAX**, **MIN**, **COUNT** ## Usage ### Currently available relations for join queries * **BelongsTo** * **HasOne** * **HasMany** ### New clauses for eloquent builder on BelongsTo and HasOne relations : **joinRelations($relations, $leftJoin = null)** * ***$relations*** which relations to join * ***$leftJoin*** use **left join** or **inner join**, default **left join** **orderByJoin($column, $direction = 'asc', $aggregateMethod = null)** * ***$column*** and ***$direction*** arguments are the same as in default eloquent **orderBy()** * ***$aggregateMethod*** argument defines which aggregate method to use ( **SUM**, **AVG**, **MAX**, **MIN**, **COUNT**), default **MAX** **whereJoin($column, $operator, $value, $boolean = 'and')** * arguments are the same as in default eloquent **where()** **orWhereJoin($column, $operator, $value)** * arguments are the same as in default eloquent **orWhere()** **whereInJoin($column, $values, $boolean = 'and', $not = false)** * arguments are the same as in default eloquent **whereIn()** **whereNotInJoin($column, $values, $boolean = 'and')** * arguments are the same as in default eloquent **whereNotIn()** **orWhereInJoin($column, $values)** * arguments are the same as in default eloquent **orWhereIn()** **orWhereNotInJoin($column, $values)** * arguments are the same as in default eloquent **orWhereNotIn()** ### Allowed clauses on BelongsTo, HasOne and HasMany relations on which you can use join clauses on the query * Relations that you want to use for join queries can only have these clauses : **where**, **orWhere**, **withTrashed**, **onlyTrashed**, **withoutTrashed**. * Clauses **where** and **orWhere** can only have these variations ** **->where($column, $operator, $value)** ** **->where([$column => $value])** * Closures are not allowed. * Other clauses like **whereHas**, **orderBy** etc. are not allowed. * 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. Allowed relation: ``` public function locationPrimary() { return $this->hasOne(Location::class) ->where('is_primary', '=', 1) ->orWhere('is_primary', '=', 1) ->withTrashed(); } ``` Not allowed relation: ``` public function locationPrimary() { return $this->hasOne(Location::class) ->where('is_primary', '=', 1) ->orWhere('is_primary', '=', 1) ->withTrashed() ->whereHas('state', function($query){return $query;} ->orderBy('name') ->where(function($query){ return $query->where('is_primary', '=', 1); }); } ``` 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. You might get a picture that there are too many rules and restriction, but it is really not like that. Don't worry, if you do create the query that is not allowed appropriate exception will be thrown and you will know what happened. ### Other * If the model uses the SoftDelete trait, where deleted_at != null will be automatically applied * You can combine new clauses unlimited times * If you combine clauses more times on same relation package will join related table only once ``` Seller::whereJoin('city.title', '=', 'test') ->orWhereJoin('city.title', '=', 'test2'); ``` * You can call new clauses inside closures ``` Seller::where(function ($query) { $query ->whereJoin('city.title', '=', 'test') ->orWhereJoin('city.title', '=', 'test2'); }); ``` * You can combine join clauses e.g. whereJoin() with eloquent clauses e.g. orderBy() ``` Seller::whereJoin('title', '=', 'test') ->whereJoin('city.title', '=', 'test') ->orderByJoin('city.title') ->get(); ``` ## See action on real example Database schema : ![Database schema](https://raw.githubusercontent.com/fico7489/laravel-eloquent-join/master/readme/era.png) Models : ``` class Seller extends BaseModel { public function locations() { return $this->hasMany(Location::class); } public function locationPrimary() { return $this->hasOne(Location::class) ->where('is_primary', '=', 1); } public function city() { return $this->belongsTo(City::class); } ``` ``` class Location extends BaseModel { public function locationAddressPrimary() { return $this->hasOne(LocationAddress::class) ->where('is_primary', '=', 1); } ``` ``` class City extends BaseModel { public function state() { return $this->belongsTo(State::class); } } ``` ### Join ##### Join BelongsTo ```Seller::joinRelations('city')``` ##### Join HasOne ```Seller::joinRelations('locationPrimary')``` ##### Join HasMany ```Seller::joinRelations('locations')``` ##### Join Mixed ```Seller::joinRelations('city.state')``` ### Join (mix left join) ```Seller::joinRelations('city', true)->joinRelations('city.state', false)``` ### Join (multiple relationships) ```Seller::join(['city.state', 'locations'])``` ### Ordering ##### Order BelongsTo ```Seller::orderByJoin('city.title')``` ##### Order HasOne ```Seller::orderByJoin('locationPrimary.address')``` ##### Order HasMany ```Seller::orderByJoin('locations.title')``` ##### Order Mixed ```Seller::orderByJoin('city.state.title')``` ### Ordering (special cases with aggregate functions) ##### Order by relation count ```Seller::orderByJoin('locations.id', 'asc', 'COUNT')``` ##### Order by relation field SUM ```Seller::orderByJoin('locations.is_primary', 'asc', 'SUM')``` ##### Order by relation field AVG ```Seller::orderByJoin('locations.is_primary', 'asc', 'AVG')``` ##### Order by relation field MAX ```Seller::orderByJoin('locations.is_primary', 'asc', 'MAX')``` ##### Order by relation field MIN ```Seller::orderByJoin('locations.is_primary', 'asc', 'MIN')``` ### Filtering (where or orWhere) ##### Filter BelongsTo ```Seller::whereJoin('city.title', '=', 'test')``` ##### Filter HasOne ```Seller::whereJoin('locationPrimary.address', '=', 'test')``` ##### Filter HasMany ```Seller::whereJoin('locations.title', '=', 'test')``` ##### Filter Mixed ```Seller::whereJoin('city.state.title', '=', 'test')``` ### Relation count ``` $sellers = Seller::setAppendRelationsCount(true)->join('locations', '=', 'test') ->get(); foreach ($sellers as $seller){ echo 'Number of location = ' . $seller->locations_count; } ``` ### Filter (mix left join) ``` Seller::joinRelations('city', true) ->joinRelations('city.state', false) ->whereJoin('city.id', '=', 1) ->orWhereJoin('city.state.id', '=', 1) ``` ## Generated queries Query : ``` Order::whereJoin('seller.id', '=', 1)->get(); ``` Sql : ``` select "orders".* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "sellers"."id" = ? and "orders"."deleted_at" is null group by "orders"."id" ``` Query : ``` Order::orderByJoin('seller.id', '=', 1)->get(); ``` Sql : ``` select "orders".*, MAX(sellers.id) as sort from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "orders"."deleted_at" is null group by "orders"."id" order by sort asc ``` ## Elegance of package Lets look how first example from documentation now looks like. This code : ``` $posts = Post::select('posts.*') ->join('categories', 'categories.id', '=', 'posts.category_id') ->groupBy('posts.id') ->where('categories.deleted_at', '=', null) ->orderBy('categories.name'); if(request()->get('date')){ $posts->where('date', $date) } $posts = $posts->get(); ``` is now : ``` $posts = Post::orderByJoin('category.name'); if(request()->get('date')){ $posts->where('posts.date', $date) } $posts = $posts->get(); ``` Both snippets do the same thing. ## Tests This package is well covered with tests. If you want run tests just run **composer update** and then run tests with **"vendor/bin/phpunit"** ## Contribution Feel free to create new issue for : * bug * notice * request new feature * question * clarification * etc... License ---- MIT **Free Software, Hell Yeah!** ================================================ FILE: composer.json ================================================ { "name": "fico7489/laravel-eloquent-join", "description": "This package introduces the join magic for eloquent models and relations.", "keywords": [ "laravel join", "laravel eloquent join", "laravel sort join", "laravel where join", "laravel join relation" ], "homepage": "https://github.com/fico7489/laravel-eloquent-join", "support": { "issues": "https://github.com/fico7489/laravel-eloquent-join/issues", "source": "https://github.com/fico7489/laravel-eloquent-join" }, "license": "MIT", "authors": [ { "name": "Filip Horvat", "email": "filip.horvat@am2studio.hr", "homepage": "http://am2studio.hr", "role": "Developer" } ], "require": { "illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0" }, "require-dev": { "orchestra/testbench": "*", "friendsofphp/php-cs-fixer" : "*", "phpunit/phpunit": "*" }, "autoload": { "psr-4": { "Fico7489\\Laravel\\EloquentJoin\\": "src" } }, "autoload-dev": { "psr-4": { "Fico7489\\Laravel\\EloquentJoin\\Tests\\": "tests/" } }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: phpunit.xml ================================================ ./tests/ ./src ./tests ./vendor ================================================ FILE: src/EloquentJoinBuilder.php ================================================ select(...) is already called on builder (we want only one groupBy()) protected $selected = false; //store joined tables, we want join table only once (e.g. when you call orderByJoin more time) protected $joinedTables = []; //store clauses on relation for join public $relationClauses = []; //query methods public function where($column, $operator = null, $value = null, $boolean = 'and') { if ($column instanceof \Closure) { $query = $this->model->newModelQuery(); $baseBuilderCurrent = $this->baseBuilder ? $this->baseBuilder : $this; $query->baseBuilder = $baseBuilderCurrent; $column($query); $this->query->addNestedWhereQuery($query->getQuery(), $boolean); } else { $this->query->where(...func_get_args()); } return $this; } public function whereJoin($column, $operator, $value, $boolean = 'and') { $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); return $this->where($column, $operator, $value, $boolean); } public function orWhereJoin($column, $operator, $value) { $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); return $this->orWhere($column, $operator, $value); } public function whereInJoin($column, $values, $boolean = 'and', $not = false) { $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); return $this->whereIn($column, $values, $boolean, $not); } public function whereNotInJoin($column, $values, $boolean = 'and') { $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); return $this->whereNotIn($column, $values, $boolean); } public function orWhereInJoin($column, $values) { $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); return $this->orWhereIn($column, $values); } public function orWhereNotInJoin($column, $values) { $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); return $this->orWhereNotIn($column, $values); } public function orderByJoin($column, $direction = 'asc', $aggregateMethod = null) { $direction = strtolower($direction); $this->checkDirection($direction); $dotPos = strrpos($column, '.'); $query = $this->baseBuilder ? $this->baseBuilder : $this; $column = $query->performJoin($column); if (false !== $dotPos) { //order by related table field $aggregateMethod = $aggregateMethod ? $aggregateMethod : $this->aggregateMethod; $this->checkAggregateMethod($aggregateMethod); $sortsCount = count($this->query->orders ?? []); $sortAlias = 'sort'.(0 == $sortsCount ? '' : ($sortsCount + 1)); $grammar = \DB::query()->getGrammar(); $query->selectRaw($aggregateMethod.'('.$grammar->wrap($column).') as '.$sortAlias); return $this->orderByRaw($sortAlias.' '.$direction); } //order by base table field return $this->orderBy($column, $direction); } /** * Joining relations. * * @param string|array $relations * @param bool|null $leftJoin * * @return $this * * @throws InvalidRelation */ public function joinRelations($relations, $leftJoin = null) { $leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin; $query = $this->baseBuilder ? $this->baseBuilder : $this; if (is_array($relations)) { foreach ($relations as $relation) { $query->joinRelations($relation, $leftJoin); } } else { $query->performJoin($relations.'.FAKE_FIELD', $leftJoin); } return $this; } //helpers methods protected function performJoin($relations, $leftJoin = null) { //detect join method $leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin; $joinMethod = $leftJoin ? 'leftJoin' : 'join'; //detect current model data $relations = explode('.', $relations); $column = end($relations); $baseModel = $this->getModel(); $baseTable = $baseModel->getTable(); $basePrimaryKey = $baseModel->getKeyName(); $currentModel = $baseModel; $currentTableAlias = $baseTable; $relationsAccumulated = []; foreach ($relations as $relation) { if ($relation == $column) { //last item in $relations argument is sort|where column break; } /** @var Relation $relatedRelation */ $relatedRelation = $currentModel->$relation(); $relatedModel = $relatedRelation->getRelated(); $relatedPrimaryKey = $relatedModel->getKeyName(); $relatedTable = $relatedModel->getTable(); $relatedTableAlias = $this->useTableAlias ? sha1($relatedTable.rand()) : $relatedTable; $relationsAccumulated[] = $relatedTableAlias; $relationAccumulatedString = implode('_', $relationsAccumulated); //relations count if ($this->appendRelationsCount) { $this->selectRaw('COUNT('.$relatedTableAlias.'.'.$relatedPrimaryKey.') as '.$relationAccumulatedString.'_count'); } if (!in_array($relationAccumulatedString, $this->joinedTables)) { $joinQuery = $relatedTable.($this->useTableAlias ? ' as '.$relatedTableAlias : ''); if ($relatedRelation instanceof BelongsToJoin) { $relatedKey = is_callable([$relatedRelation, 'getQualifiedForeignKeyName']) ? $relatedRelation->getQualifiedForeignKeyName() : $relatedRelation->getQualifiedForeignKey(); $relatedKey = last(explode('.', $relatedKey)); $ownerKey = is_callable([$relatedRelation, 'getOwnerKeyName']) ? $relatedRelation->getOwnerKeyName() : $relatedRelation->getOwnerKey(); $this->$joinMethod($joinQuery, function ($join) use ($relatedRelation, $relatedTableAlias, $relatedKey, $currentTableAlias, $ownerKey) { $join->on($relatedTableAlias.'.'.$ownerKey, '=', $currentTableAlias.'.'.$relatedKey); $this->joinQuery($join, $relatedRelation, $relatedTableAlias); }); } elseif ($relatedRelation instanceof HasOneJoin || $relatedRelation instanceof HasManyJoin) { $relatedKey = $relatedRelation->getQualifiedForeignKeyName(); $relatedKey = last(explode('.', $relatedKey)); $localKey = $relatedRelation->getQualifiedParentKeyName(); $localKey = last(explode('.', $localKey)); $this->$joinMethod($joinQuery, function ($join) use ($relatedRelation, $relatedTableAlias, $relatedKey, $currentTableAlias, $localKey) { $join->on($relatedTableAlias.'.'.$relatedKey, '=', $currentTableAlias.'.'.$localKey); $this->joinQuery($join, $relatedRelation, $relatedTableAlias); }); } else { throw new InvalidRelation(); } } $currentModel = $relatedModel; $currentTableAlias = $relatedTableAlias; $this->joinedTables[] = implode('_', $relationsAccumulated); } if (!$this->selected && count($relations) > 1) { $this->selected = true; $this->selectRaw($baseTable.'.*'); $this->groupBy($baseTable.'.'.$basePrimaryKey); } return $currentTableAlias.'.'.$column; } protected function joinQuery($join, $relation, $relatedTableAlias) { /** @var Builder $relationQuery */ $relationBuilder = $relation->getQuery(); //apply clauses on relation if (isset($relationBuilder->relationClauses)) { foreach ($relationBuilder->relationClauses as $clause) { foreach ($clause as $method => $params) { $this->applyClauseOnRelation($join, $method, $params, $relatedTableAlias); } } } //apply global SoftDeletingScope foreach ($relationBuilder->scopes as $scope) { if ($scope instanceof SoftDeletingScope) { $this->applyClauseOnRelation($join, 'withoutTrashed', [], $relatedTableAlias); } else { throw new InvalidRelationGlobalScope(); } } } private function applyClauseOnRelation(JoinClause $join, string $method, array $params, string $relatedTableAlias) { if (in_array($method, ['where', 'orWhere'])) { try { if (is_array($params[0])) { foreach ($params[0] as $k => $param) { $params[0][$relatedTableAlias.'.'.$k] = $param; unset($params[0][$k]); } } elseif (is_callable($params[0])) { throw new InvalidRelationWhere(); } else { $params[0] = $relatedTableAlias.'.'.$params[0]; } call_user_func_array([$join, $method], $params); } catch (\Exception $e) { throw new InvalidRelationWhere(); } } elseif (in_array($method, ['withoutTrashed', 'onlyTrashed', 'withTrashed'])) { if ('withTrashed' == $method) { //do nothing } elseif ('withoutTrashed' == $method) { call_user_func_array([$join, 'where'], [$relatedTableAlias.'.deleted_at', '=', null]); } elseif ('onlyTrashed' == $method) { call_user_func_array([$join, 'where'], [$relatedTableAlias.'.deleted_at', '<>', null]); } } else { throw new InvalidRelationClause(); } } private function checkAggregateMethod($aggregateMethod) { if (!in_array($aggregateMethod, [ self::AGGREGATE_SUM, self::AGGREGATE_AVG, self::AGGREGATE_MAX, self::AGGREGATE_MIN, self::AGGREGATE_COUNT, ])) { throw new InvalidAggregateMethod(); } } private function checkDirection($direction) { if (!in_array($direction, ['asc', 'desc'], true)) { throw new InvalidDirection(); } } //getters and setters public function isUseTableAlias(): bool { return $this->useTableAlias; } public function setUseTableAlias(bool $useTableAlias) { $this->useTableAlias = $useTableAlias; return $this; } public function isLeftJoin(): bool { return $this->leftJoin; } public function setLeftJoin(bool $leftJoin) { $this->leftJoin = $leftJoin; return $this; } public function isAppendRelationsCount(): bool { return $this->appendRelationsCount; } public function setAppendRelationsCount(bool $appendRelationsCount) { $this->appendRelationsCount = $appendRelationsCount; return $this; } public function getAggregateMethod(): string { return $this->aggregateMethod; } public function setAggregateMethod(string $aggregateMethod) { $this->checkAggregateMethod($aggregateMethod); $this->aggregateMethod = $aggregateMethod; return $this; } } ================================================ FILE: src/Exceptions/InvalidAggregateMethod.php ================================================ where($column, $operator, $value) and ->where([$column => $value]).'; } ================================================ FILE: src/Relations/BelongsToJoin.php ================================================ useTableAlias)) { $newEloquentBuilder->setUseTableAlias($this->useTableAlias); } if (isset($this->appendRelationsCount)) { $newEloquentBuilder->setAppendRelationsCount($this->appendRelationsCount); } if (isset($this->leftJoin)) { $newEloquentBuilder->setLeftJoin($this->leftJoin); } if (isset($this->aggregateMethod)) { $newEloquentBuilder->setAggregateMethod($this->aggregateMethod); } return $newEloquentBuilder; } } ================================================ FILE: src/Traits/ExtendRelationsTrait.php ================================================ getQuery() instanceof EloquentJoinBuilder) { $this->getQuery()->relationClauses[] = [$method => $parameters]; } return parent::__call($method, $parameters); } } ================================================ FILE: tests/Models/BaseModel.php ================================================ belongsTo(State::class); } public function zipCodePrimary() { return $this->hasOne(ZipCode::class) ->where('is_primary', '=', 1); } public function sellers() { return $this->belongsToMany(Seller::class, 'locations', 'seller_id', 'city_id'); } public function zipCodes() { return $this->hasMany(ZipCode::class); } } ================================================ FILE: tests/Models/Integration.php ================================================ belongsTo(Seller::class, 'id_seller_foreign', 'id_seller_primary'); } public function sellerOwner() { return $this->belongsTo(Seller::class, 'id_seller_foreign', 'id_seller_owner'); } } ================================================ FILE: tests/Models/Key/Seller.php ================================================ hasOne(Location::class, 'id_seller_foreign', 'id_seller_primary'); } public function locations() { return $this->hasMany(Location::class, 'id_seller_foreign', 'id_seller_primary'); } public function locationOwner() { return $this->hasOne(Location::class, 'id_seller_foreign', 'id_seller_owner'); } public function locationsOwner() { return $this->hasMany(Location::class, 'id_seller_foreign', 'id_seller_owner'); } } ================================================ FILE: tests/Models/Location.php ================================================ belongsTo(Seller::class); } public function city() { return $this->belongsTo(City::class); } public function locationAddressPrimary() { return $this->hasOne(LocationAddress::class) ->where('is_primary', '=', 1); } public function integrations() { return $this->hasMany(Integration::class); } } ================================================ FILE: tests/Models/LocationAddress.php ================================================ hasMany(User::class); } } ================================================ FILE: tests/Models/LocationWithGlobalScope.php ================================================ belongsTo(Seller::class); } } ================================================ FILE: tests/Models/OrderItem.php ================================================ belongsTo(Order::class); } public function orderWithTrashed() { return $this->belongsTo(Order::class, 'order_id') ->withTrashed(); } public function orderOnlyTrashed() { return $this->belongsTo(Order::class, 'order_id') ->onlyTrashed(); } } ================================================ FILE: tests/Models/Seller.php ================================================ hasOne(Location::class) ->where('is_primary', '=', 0) ->where('is_secondary', '=', 0); } public function locations() { return $this->hasMany(Location::class); } public function locationPrimary() { return $this->hasOne(Location::class) ->where('is_primary', '=', 1); } public function locationPrimaryInvalid() { return $this->hasOne(Location::class) ->where('is_primary', '=', 1) ->orderBy('is_primary'); } public function locationPrimaryInvalid2() { return $this->hasOne(Location::class) ->where(function ($query) { return $query->where(['id' => 1]); }); } public function locationPrimaryInvalid3() { return $this->hasOne(LocationWithGlobalScope::class); } public function locationSecondary() { return $this->hasOne(Location::class) ->where('is_secondary', '=', 1); } public function locationPrimaryOrSecondary() { return $this->hasOne(Location::class) ->where('is_primary', '=', 1) ->orWhere('is_secondary', '=', 1); } public function city() { return $this->belongsTo(City::class); } } ================================================ FILE: tests/Models/State.php ================================================ hasMany(City::class); } } ================================================ FILE: tests/Models/User.php ================================================ where('test', '=', 'test'); } } ================================================ FILE: tests/ServiceProvider.php ================================================ loadMigrationsFrom(__DIR__.'/database/migrations/'); } protected function loadMigrationsFrom($path) { $_ENV['type'] = 'sqlite'; //sqlite, mysql, pgsql \Artisan::call('migrate', ['--database' => $_ENV['type']]); $migrator = $this->app->make('migrator'); $migrator->run($path); } } ================================================ FILE: tests/TestCase.php ================================================ 1]); $seller2 = Seller::create(['title' => 2]); $seller3 = Seller::create(['title' => 3]); $seller4 = Seller::create(['title' => 4]); Location::create(['address' => 1, 'seller_id' => $seller->id]); Location::create(['address' => 2, 'seller_id' => $seller2->id]); Location::create(['address' => 3, 'seller_id' => $seller3->id]); Location::create(['address' => 3, 'seller_id' => $seller3->id]); Location::create(['address' => 4, 'seller_id' => $seller3->id, 'is_primary' => 1]); Location::create(['address' => 5, 'seller_id' => $seller3->id, 'is_secondary' => 1]); $order = Order::create(['number' => '1', 'seller_id' => $seller->id]); $order2 = Order::create(['number' => '2', 'seller_id' => $seller2->id]); $order3 = Order::create(['number' => '3', 'seller_id' => $seller3->id]); OrderItem::create(['name' => '1', 'order_id' => $order->id]); OrderItem::create(['name' => '2', 'order_id' => $order2->id]); OrderItem::create(['name' => '3', 'order_id' => $order3->id]); $this->startListening(); } protected function startListening() { \DB::enableQueryLog(); } protected function fetchLastLog() { $log = \DB::getQueryLog(); return end($log); } protected function fetchQuery() { $query = $this->fetchLastLog()['query']; $bindings = $this->fetchLastLog()['bindings']; foreach ($bindings as $binding) { $binding = is_string($binding) ? ('"'.$binding.'"') : $binding; $query = preg_replace('/\?/', $binding, $query, 1); } return $query; } protected function fetchBindings() { return $this->fetchLastLog()['bindings']; } protected function getEnvironmentSetUp($app) { // Setup default database to use sqlite :memory: $app['config']->set('database.connections.sqlite', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); $app['config']->set('database.connections.mysql', [ 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'join', 'username' => 'root', 'password' => '', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'strict' => true, ]); $app['config']->set('database.connections.pgsql', [ 'driver' => 'pgsql', 'host' => 'localhost', 'database' => 'join', 'username' => 'postgres', 'password' => 'root', 'charset' => 'utf8', 'prefix' => '', 'schema' => 'public', 'sslmode' => 'prefer', ]); $app['config']->set('database.default', env('type', 'sqlite')); } protected function getPackageProviders($app) { return [ServiceProvider::class]; } protected function assertQueryMatches($expected, $actual) { $actual = preg_replace('/\s\s+/', ' ', $actual); $actual = str_replace(['\n', '\r'], '', $actual); $expected = preg_replace('/\s\s+/', ' ', $expected); $expected = str_replace(['\n', '\r'], '', $expected); $expected = '/'.$expected.'/'; $expected = preg_quote($expected); if ('mysql' == $_ENV['type']) { $expected = str_replace(['"'], '`', $expected); } $this->assertMatchesRegularExpression($expected, $actual); } } ================================================ FILE: tests/Tests/AggregateJoinTest.php ================================================ orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_SUM) ->get(); $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_SUM, $this->queryTest); $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testSum() { Order::joinRelations('seller') ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_AVG) ->get(); $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_AVG, $this->queryTest); $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testMax() { Order::joinRelations('seller') ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_MAX) ->get(); $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_MAX, $this->queryTest); $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testMin() { Order::joinRelations('seller') ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_MIN) ->get(); $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_MIN, $this->queryTest); $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testCount() { Order::joinRelations('seller') ->orderByJoin('seller.id', 'asc', EloquentJoinBuilder::AGGREGATE_COUNT) ->get(); $queryTest = str_replace(EloquentJoinBuilder::AGGREGATE_SUM, EloquentJoinBuilder::AGGREGATE_COUNT, $this->queryTest); $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/AppendRelationsCountTest.php ================================================ joinRelations('seller.locationPrimary.locationAddressPrimary')->get(); $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.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 and "locations"."deleted_at" is null left join "location_addresses" on "location_addresses"."location_id" = "locations"."id" and "location_addresses"."is_primary" = 1 and "location_addresses"."deleted_at" is null where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/JoinRelationsTest.php ================================================ get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/OrWhereInTest.php ================================================ whereInJoin('seller.id', [1, 2]) ->orWhereInJoin('seller.id', [3, 4]) ->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where ("sellers"."id" in (1, 2) or "sellers"."id" in (3, 4)) and "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/OrWhereNotInTest.php ================================================ whereInJoin('seller.id', [1, 2]) ->orWhereNotInJoin('seller.id', [3, 4]) ->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where ("sellers"."id" in (1, 2) or "sellers"."id" not in (3, 4)) and "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/OrWhereTest.php ================================================ whereJoin('seller.id', '=', 1) ->orWhereJoin('seller.id', '=', 2) ->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where ("sellers"."id" = 1 or "sellers"."id" = 2) and "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/OrderByTest.php ================================================ orderByJoin('seller.id', 'asc') ->get(); $queryTest = 'select orders.*, MAX("sellers"."id") as sort from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "orders"."deleted_at" is null group by "orders"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testOrderByMultiple() { Order::joinRelations('seller') ->orderByJoin('seller.id', 'asc') ->orderByJoin('seller.title', 'desc') ->get(); $queryTest = 'select orders.*, MAX("sellers"."id") as sort, MAX("sellers"."title") as sort2 from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "orders"."deleted_at" is null group by "orders"."id" order by sort asc, sort2 desc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/WhereInTest.php ================================================ whereInJoin('seller.id', [1, 2]) ->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "sellers"."id" in (1, 2) and "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/WhereNotInTest.php ================================================ whereNotInJoin('seller.id', [1, 2]) ->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "sellers"."id" not in (1, 2) and "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Clauses/WhereTest.php ================================================ whereJoin('seller.id', '=', 1) ->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "sellers"."id" = 1 and "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/ClosureOnRelationTest.php ================================================ 0', 'is_secondary' => 0] $items = Seller::orderByJoin('location.id', 'desc')->get(); $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 0 and "locations"."is_secondary" = 0 and "locations"."deleted_at" is null group by "sellers"."id" order by sort desc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); //locationPrimary have one where ['is_primary => 1'] $items = Seller::orderByJoin('locationPrimary.id', 'desc')->get(); $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 and "locations"."deleted_at" is null group by "sellers"."id" order by sort desc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); //locationPrimary have one where ['is_secondary => 1'] $items = Seller::orderByJoin('locationSecondary.id', 'desc')->get(); $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_secondary" = 1 and "locations"."deleted_at" is null group by "sellers"."id" order by sort desc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); //locationPrimary have one where ['is_primary => 1'] and one orWhere ['is_secondary => 1'] $items = Seller::orderByJoin('locationPrimaryOrSecondary.id', 'desc')->get(); $queryTest = 'select sellers.*, MAX("locations"."id") as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 or "locations"."is_secondary" = 1 and "locations"."deleted_at" is null group by "sellers"."id" order by sort desc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testWhereOnRelationWithoutOrderByJoin() { $seller = Seller::find(1); $seller->locationPrimary; $queryTest = 'select * from "locations" where "locations"."seller_id" = 1 and "locations"."seller_id" is not null and "is_primary" = 1 and "locations"."deleted_at" is null limit 1'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); $seller->locationPrimary()->where(['is_secondary' => 1])->get(); $queryTest = 'select * from "locations" where "locations"."seller_id" = 1 and "locations"."seller_id" is not null and "is_primary" = 1 and ("is_secondary" = 1) and "locations"."deleted_at" is null'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/ClosureTest.php ================================================ orWhereJoin('order.id', '=', 1) ->orWhereJoin('order.id', '=', 2); })->get(); $queryTest = 'select order_items.* from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null where ("orders"."id" = 1 or "orders"."id" = 2) and "order_items"."deleted_at" is null'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testNestTwo() { OrderItem::where(function ($query) { $query ->orWhereJoin('order.id', '=', 1) ->orWhereJoin('order.id', '=', 2) ->where(function ($query) { $query->orWhereJoin('order.seller.locationPrimary.id', '=', 3); }); })->get(); $queryTest = 'select order_items.* from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null left join "sellers" on "sellers"."id" = "orders"."seller_id" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 and "locations"."deleted_at" is null where ("orders"."id" = 1 or "orders"."id" = 2 and ("locations"."id" = 3)) and "order_items"."deleted_at" is null'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/ExceptionTest.php ================================================ get(); } catch (InvalidRelation $e) { $this->assertEquals((new InvalidRelation())->message, $e->getMessage()); return; } $this->assertTrue(false); } public function testInvalidRelationWhere() { try { Seller::whereJoin('locationPrimaryInvalid2.name', '=', 'test')->get(); } catch (InvalidRelationWhere $e) { $this->assertEquals((new InvalidRelationWhere())->message, $e->getMessage()); return; } $this->assertTrue(false); } public function testInvalidRelationClause() { try { Seller::whereJoin('locationPrimaryInvalid.name', '=', 'test')->get(); } catch (InvalidRelationClause $e) { $this->assertEquals((new InvalidRelationClause())->message, $e->getMessage()); return; } $this->assertTrue(false); } public function testInvalidRelationGlobalScope() { try { Seller::whereJoin('locationPrimaryInvalid3.id', '=', 'test')->get(); } catch (InvalidRelationGlobalScope $e) { $this->assertEquals((new InvalidRelationGlobalScope())->message, $e->getMessage()); return; } $this->assertTrue(false); } public function testInvalidAggregateMethod() { try { Seller::orderByJoin('locationPrimary.id', 'asc', 'wrong')->get(); } catch (InvalidAggregateMethod $e) { $this->assertEquals((new InvalidAggregateMethod())->message, $e->getMessage()); return; } $this->assertTrue(false); } public function testOrderByInvalidDirection() { $this->expectException(InvalidDirection::class); Seller::orderByJoin('locationPrimary.id', ';DROP TABLE orders;--', 'test')->get(); } } ================================================ FILE: tests/Tests/JoinTypeTest.php ================================================ whereJoin('city.name', '=', 'test')->get(); $queryTest = 'select sellers.* from "sellers" left join "cities" on "cities"."id" = "sellers"."city_id" and "cities"."deleted_at" is null where "cities"."name" = "test"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testInnerJoin() { Seller::setLeftJoin(false)->whereJoin('city.name', '=', 'test')->get(); $queryTest = 'select sellers.* from "sellers" inner join "cities" on "cities"."id" = "sellers"."city_id" and "cities"."deleted_at" is null where "cities"."name" = "test"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testMixedJoin() { Order::joinRelations('seller', true)->joinRelations('seller.city', false)->joinRelations('seller.city.state', true)->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" inner join "cities" on "cities"."id" = "sellers"."city_id" and "cities"."deleted_at" is null left join "states" on "states"."id" = "cities"."state_id" and "states"."deleted_at" is null where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/KeysOwnerTest.php ================================================ get(); $queryTest = 'select key_orders.* from "key_orders" left join "key_sellers" on "key_sellers"."id_seller_owner" = "key_orders"."id_seller_foreign" group by "key_orders"."id_order_primary"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasOne() { Seller::joinRelations('locationOwner') ->get(); $queryTest = 'select key_sellers.* from "key_sellers" left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_owner" group by "key_sellers"."id_seller_primary"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasMany() { Seller::joinRelations('locationsOwner') ->get(); $queryTest = 'select key_sellers.* from "key_sellers" left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_owner" group by "key_sellers"."id_seller_primary"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/KeysTest.php ================================================ get(); $queryTest = 'select key_orders.* from "key_orders" left join "key_sellers" on "key_sellers"."id_seller_primary" = "key_orders"."id_seller_foreign" group by "key_orders"."id_order_primary"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasOne() { Seller::joinRelations('location') ->get(); $queryTest = 'select key_sellers.* from "key_sellers" left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_primary" group by "key_sellers"."id_seller_primary"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasMany() { Seller::joinRelations('locations') ->get(); $queryTest = 'select key_sellers.* from "key_sellers" left join "key_locations" on "key_locations"."id_seller_foreign" = "key_sellers"."id_seller_primary" group by "key_sellers"."id_seller_primary"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/OptionsTest.php ================================================ assertEquals(false, $city->newModelQuery()->isUseTableAlias()); $city->useTableAlias = true; $this->assertEquals(true, $city->newModelQuery()->isUseTableAlias()); } public function testAppendRelationsCount() { $city = new City(); $this->assertEquals(false, $city->newModelQuery()->isAppendRelationsCount()); $city->appendRelationsCount = true; $this->assertEquals(true, $city->newModelQuery()->isAppendRelationsCount()); } public function testLeftJoin() { $city = new City(); $this->assertEquals(true, $city->newModelQuery()->isLeftJoin()); $city->leftJoin = false; $this->assertEquals(false, $city->newModelQuery()->isLeftJoin()); } public function testAggregateMethod() { $city = new City(); $this->assertEquals(EloquentJoinBuilder::AGGREGATE_MAX, $city->newModelQuery()->getAggregateMethod()); $city->aggregateMethod = EloquentJoinBuilder::AGGREGATE_MIN; $this->assertEquals(EloquentJoinBuilder::AGGREGATE_MIN, $city->newModelQuery()->getAggregateMethod()); } } ================================================ FILE: tests/Tests/Relations/BelongsToTest.php ================================================ get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testBelongsToHasOne() { Order::joinRelations('seller.locationPrimary')->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 and "locations"."deleted_at" is null where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testBelongsToHasMany() { Order::joinRelations('seller.locations')->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."deleted_at" is null where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testBelongsToHasOneHasMany() { Order::joinRelations('seller.locationPrimary.integrations')->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 and "locations"."deleted_at" is null left join "integrations" on "integrations"."location_id" = "locations"."id" and "integrations"."deleted_at" is null where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testBelongsToHasManyHasOne() { Order::joinRelations('seller.locationPrimary.locationAddressPrimary')->get(); $queryTest = 'select orders.* from "orders" left join "sellers" on "sellers"."id" = "orders"."seller_id" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 1 and "locations"."deleted_at" is null left join "location_addresses" on "location_addresses"."location_id" = "locations"."id" and "location_addresses"."is_primary" = 1 and "location_addresses"."deleted_at" is null where "orders"."deleted_at" is null group by "orders"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Relations/HasManyTest.php ================================================ get(); $queryTest = 'select sellers.* from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."deleted_at" is null group by "sellers"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasManyHasOne() { Seller::joinRelations('locations.city')->get(); $queryTest = 'select sellers.* from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."deleted_at" is null left join "cities" on "cities"."id" = "locations"."city_id" and "cities"."deleted_at" is null group by "sellers"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasManyBelongsTo() { Seller::joinRelations('locations.integrations')->get(); $queryTest = 'select sellers.* from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."deleted_at" is null left join "integrations" on "integrations"."location_id" = "locations"."id" and "integrations"."deleted_at" is null group by "sellers"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/Relations/HasOneTest.php ================================================ get(); $queryTest = 'select sellers.* from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 0 and "locations"."is_secondary" = 0 and "locations"."deleted_at" is null group by "sellers"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasOneBelongsTo() { Seller::joinRelations('location.city')->get(); $queryTest = 'select sellers.* from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 0 and "locations"."is_secondary" = 0 and "locations"."deleted_at" is null left join "cities" on "cities"."id" = "locations"."city_id" and "cities"."deleted_at" is null group by "sellers"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testHasOneHasMany() { Seller::joinRelations('location.integrations')->get(); $queryTest = 'select sellers.* from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = 0 and "locations"."is_secondary" = 0 and "locations"."deleted_at" is null left join "integrations" on "integrations"."location_id" = "locations"."id" and "integrations"."deleted_at" is null group by "sellers"."id"'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/Tests/SoftDeleteTest.php ================================================ get(); $queryTest = 'select * from "order_items" where "order_items"."deleted_at" is null order by "order_items"."name" asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testNotRelatedWithoutTrashedExplicit() { OrderItem::orderByJoin('name')->withoutTrashed()->get(); $queryTest = 'select * from "order_items" where "order_items"."deleted_at" is null order by "order_items"."name" asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testNotRelatedOnlyTrashedExplicit() { OrderItem::orderByJoin('name')->onlyTrashed()->get(); $queryTest = 'select * from "order_items" where "order_items"."deleted_at" is not null order by "order_items"."name" asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testNotRelatedWithTrashedExplicit() { OrderItem::orderByJoin('name')->withTrashed()->get(); $queryTest = 'select * from "order_items" order by "order_items"."name" asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testRelatedWithoutTrashedDefault() { OrderItem::orderByJoin('order.number')->get(); $queryTest = 'select order_items.*, MAX("orders"."number") as sort from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null where "order_items"."deleted_at" is null group by "order_items"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testRelatedWithoutTrashedExplicit() { OrderItem::orderByJoin('order.number')->withoutTrashed()->get(); $queryTest = 'select order_items.*, MAX("orders"."number") as sort from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null where "order_items"."deleted_at" is null group by "order_items"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testRelatedOnlyTrashedExplicit() { OrderItem::orderByJoin('order.number')->onlyTrashed()->get(); $queryTest = 'select order_items.*, MAX("orders"."number") as sort from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null where "order_items"."deleted_at" is not null group by "order_items"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testRelatedWithTrashedExplicit() { OrderItem::orderByJoin('order.number')->withTrashed()->get(); $queryTest = 'select order_items.*, MAX("orders"."number") as sort from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null group by "order_items"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testRelatedWithTrashedOnRelation() { OrderItem::orderByJoin('orderWithTrashed.number')->get(); $queryTest = 'select order_items.*, MAX("orders"."number") as sort from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" where "order_items"."deleted_at" is null group by "order_items"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } public function testRelatedOnlyTrashedOnRelation() { OrderItem::orderByJoin('orderOnlyTrashed.number')->get(); $queryTest = 'select order_items.*, MAX("orders"."number") as sort from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is not null where "order_items"."deleted_at" is null group by "order_items"."id" order by sort asc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } } ================================================ FILE: tests/database/migrations/2017_11_04_163552_create_database.php ================================================ increments('id'); $table->string('name')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('cities', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->unsignedInteger('state_id')->nullable(); $table->foreign('state_id')->references('id')->on('states'); $table->timestamps(); $table->softDeletes(); }); Schema::create('sellers', function (Blueprint $table) { $table->increments('id'); $table->string('title')->nullable(); $table->unsignedInteger('city_id')->nullable(); $table->foreign('city_id')->references('id')->on('cities'); $table->timestamps(); $table->softDeletes(); }); Schema::create('orders', function (Blueprint $table) { $table->increments('id'); $table->string('number')->nullable(); $table->unsignedInteger('seller_id')->nullable(); $table->foreign('seller_id')->references('id')->on('sellers'); $table->timestamps(); $table->softDeletes(); }); Schema::create('order_items', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->unsignedInteger('order_id')->nullable(); $table->foreign('order_id')->references('id')->on('orders'); $table->timestamps(); $table->softDeletes(); }); Schema::create('locations', function (Blueprint $table) { $table->increments('id'); $table->string('address')->nullable(); $table->boolean('is_primary')->default(0); $table->boolean('is_secondary')->default(0); $table->unsignedInteger('seller_id')->nullable(); $table->unsignedInteger('city_id')->nullable(); $table->foreign('seller_id')->references('id')->on('sellers'); $table->foreign('city_id')->references('id')->on('cities'); $table->timestamps(); $table->softDeletes(); }); Schema::create('zip_codes', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->boolean('is_primary')->default(0); $table->unsignedInteger('city_id')->nullable(); $table->foreign('city_id')->references('id')->on('cities'); $table->timestamps(); $table->softDeletes(); }); Schema::create('location_addresses', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->boolean('is_primary')->default(0); $table->unsignedInteger('location_id')->nullable(); $table->foreign('location_id')->references('id')->on('locations'); $table->timestamps(); $table->softDeletes(); }); Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->unsignedInteger('location_address_id')->nullable(); $table->foreign('location_address_id')->references('id')->on('location_addresses'); $table->timestamps(); $table->softDeletes(); }); Schema::create('integrations', function (Blueprint $table) { $table->increments('id'); $table->string('name')->nullable(); $table->unsignedInteger('location_id')->nullable(); $table->foreign('location_id')->references('id')->on('locations'); $table->timestamps(); $table->softDeletes(); }); //for key tests Schema::create('key_orders', function (Blueprint $table) { $table->increments('id_order_primary'); $table->unsignedInteger('id_order_owner')->nullable(); $table->string('number')->nullable(); $table->unsignedInteger('id_seller_foreign')->nullable(); $table->foreign('id_seller_foreign')->references('id')->on('sellers'); }); Schema::create('key_sellers', function (Blueprint $table) { $table->increments('id_seller_primary'); $table->unsignedInteger('id_seller_owner')->nullable(); $table->string('title')->nullable(); }); Schema::create('key_locations', function (Blueprint $table) { $table->increments('id_location_primary'); $table->unsignedInteger('id_location_owner')->nullable(); $table->string('address')->nullable(); $table->unsignedInteger('id_seller_foreign')->nullable(); $table->foreign('id_seller_foreign')->references('id')->on('sellers'); }); } /** * Reverse the migrations. */ public function down() { Schema::drop('users'); Schema::drop('sellers'); Schema::drop('order_items'); Schema::drop('locations'); Schema::drop('cities'); Schema::drop('zip_codes'); Schema::drop('states'); Schema::drop('location_addresses'); Schema::drop('integrations'); Schema::drop('orders'); //for key tests Schema::drop('key_orders'); Schema::drop('key_sellers'); Schema::drop('key_locations'); } }