diff --git a/README.md b/README.md index 731dd46..f26e999 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # Laravel Eloquent Join -This package introduces the join capability for sorting and filtering on eloquent relations. +This package introduces the join magic for eloquent models and relations. -## Eloquent Problems +## Introduction -You can't perform sorting on the relationship field 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: +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.*') @@ -21,46 +25,55 @@ $posts = $posts->get(); ``` 1.The first problem is that you need to worry about select. - +``` ->select('posts.*') - -reason : without select() id from category can be selected and hydrated into Post model. +``` +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. +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. +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 : post and category can have "date" attribute and in that case without selecting attribute with table "ambiguous column" error will be thrown. +``` +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 : - +``` +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 out of the box. -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. +``` +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) -## Version Compatibility +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 | 3.* | yes | master +| >= 5.5.0 | 4.* | yes | master | < 5.5.0 | - | no | - -## Install +Package is also tested for SQLite, MySql and PostgreSql + +## Installation & setup 1.Install package with composer ``` @@ -81,12 +94,46 @@ abstract class BaseModel extends Model ... ``` +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 -##### Use table alias -Should we use alias for joined tables (default = false) +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 : ``` @@ -97,65 +144,104 @@ select "sellers".* from "sellers" With **false** query will look like this : ``` -select "sellers".* from "sellers" +select "sellers".* + from "sellers" left join "locations" ... ``` -Set option in your base model : -``` - public function __construct(array $attributes = []) - { - parent::__construct($attributes); - - $this->useTableAlias = true; - } -``` +Alias is a randomly generated string. -## Instructions for use +#### **appendRelationsCount** -### Currently available relations for join queries - -* **BelongsTo** -* **HasOne**. +Should we automatically append relation count field to results (default = false) -### New clauses for eloquent builder on BelongsTo and HasOne relations - -* **orderByJoin($column, $sortBy = 'asc', $leftJoin = true)** +With **true** query will look like this : +``` +select "sellers".*, count(locations.id) AS locations_count + from "sellers" + left join "locations" as "5b5c093d2e00f" + ... +``` - ***$column*** argument is same as in default eloquent orderBy() - - ***$direction*** argument is same as in default eloquent orderBy() - - ***$leftJoin*** argument defines if eloquent should perform left join or inner join, can be true or false - -* **whereJoin($column, $operator = null, $value = null, $boolean = 'and')** +Each **relation** is glued with an underscore and at the end **_count** prefix is added. For example for - ***$column***, ***$operator***, ***$value*** and ***$boolean*** arguments are the same as in default eloquent where() + ->joinRelations('seller.locations') -* **orWhereJoin($column, $operator = null, $value)** +field would be __seller_locations_count__ - ***$column***, ***$operator*** and ***$value*** arguments are the same as in default eloquent orWhere() +#### **leftJoin** -### Rules for column parameter in whereJoin, orWhereJoin and orderByJoin +Should we use **inner join** or **left join** (default = true) -* current table attributes ``` -->where('title', '=', 'test') +select "sellers".* + from "sellers" + inner join "locations" + ... ``` -* related table attributes (relationship names with dots) + +vs + ``` -->where('relationName.title', '=', 'test') +select "sellers".* + from "sellers" + left join "locations" + ... ``` -* related tables can be nested unlimited with any combination of HasOne and BelongsTo relations, they only need to meet **relation rules** for join queries. + +#### **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. + ``` -->where('relationName.relationNameSecond.title', '=', 'test') +select "sellers".*, MAX("locations" ."number") AS sort + from "sellers" + left join "locations" + group by "locations" ."id" + order by sort + ... ``` -### Allowed clauses on BelongsTo and HasOne relations on which you can use join clauses on the query +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')** + +* ***$column***, ***$operator***, ***$value*** and ***$boolean*** arguments are the same as in default eloquent **where()** + +**orWhereJoin($column, $operator, $value)** + +* ***$column***, ***$operator*** and ***$value*** arguments are the same as in default eloquent **orWhere()** + +### 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 this clauses : **where**, **orWhere**, **withTrashed**, **onlyTrashed**, **withoutTrashed**. -* Clauses **where** and **orWhere** can only have this variations **->where($column, $operator, $value)** and **->where([$column => $value])**, closures are not allowed. +* Clauses **where** and **orWhere** can only have this 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 that case, you can't use those relations for join queries. @@ -179,24 +265,47 @@ public function locationPrimary() ->orWhere('is_primary', '=', 1) ->withTrashed() ->whereHas('state', function($query){return $query;} - ->orderBy('name'); + ->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 to many rules and restriction, but it is really not like that. +Don't worry, if you anyway create the query that is not allowed appropriate exception will be thrown and you will know what happened. + ### Other +* If the model uses 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 -* You can combine join clauses e.g. whereJoin() with elouent clauses e.g. orderBy() ``` -Seller::whereJoin('title', 'test')->whereJoin('city.title', 'test')->orderByJoin('city.title')->get(); +Seller::whereJoin('city.title', '=', 'test') + ->orWhereJoin('city.title', '=', 'test2'); ``` -You might get a picture that there are to many rules and restriction, but it is really not like that. -Don't worry, if you anyway create the query that is not allowed appropriate exception will be thrown and you will know what happened. +* 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 @@ -209,6 +318,11 @@ Models : ``` class Seller extends BaseModel { + public function locations() + { + return $this->hasMany(Location::class); + } + public function locationPrimary() { return $this->hasOne(Location::class) @@ -229,10 +343,6 @@ class Location extends BaseModel ->where('is_primary', '=', 1); } -``` -``` -class LocationAddress extends BaseModel -{ ``` ``` class City extends BaseModel @@ -243,50 +353,124 @@ class City extends BaseModel } } ``` -``` -class State extends BaseModel -{ -``` -### Ordering +### Join + +##### Join BelongsTo +```Seller::joinRelations('city')``` + +##### Join HasOne +```Seller::joinRelations('locationPrimary')``` + +##### Join HasMany +```Seller::joinRelations('locations')``` + +##### Join Mixed +```Seller::joinRelations('city.state')``` -##### Order sellers by seller title -```Seller::orderByJoin('title')``` +### Join (mix left join) -##### Order sellers by city name +```Seller::joinRelations('city', true)->joinRelations('city.state', false)``` + +### Ordering + +##### Order BelongsTo ```Seller::orderByJoin('city.title')``` -##### Order sellers by state name +##### Order HasOne +```Seller::orderByJoin('locationPrimary.address')``` + +##### Order HasMany +```Seller::orderByJoin('locations.title')``` + +##### Order Mixed ```Seller::orderByJoin('city.state.title')``` -##### Order sellers by primary location address -```Seller::orderByJoin('locationPrimary.address')``` +### 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 sellers by locationAddress name of primary location -```Seller::orderByJoin('locationPrimary.locationAddressPrimary.address')``` +##### Order by relation field AVG +```Seller::orderByJoin('locations.is_primary', 'asc', 'AVG')``` -##### You can also combine orderBy more times -```Seller::orderByJoin('title')->orderBy('city.title')``` +##### Order by relation field MAX +```Seller::orderByJoin('locations.is_primary', 'asc', 'MAX')``` -### Filtering +##### Order by relation field MIN +```Seller::orderByJoin('locations.is_primary', 'asc', 'MIN')``` -##### Filter sellers which have title = 'test' -```Seller::whereJoin('title', 'test')``` +### Filtering (where or orWhere) -##### Filter sellers which have city name = 'test' +##### Filter BelongsTo ```Seller::whereJoin('city.title', '=', 'test')``` -##### Filter sellers which have state name = 'test' +##### Filter HasOne +```Seller::whereJoin('locationPrimary.address', '=', 'test')``` + +##### Filter HasMany +```Seller::whereJoin('locations.title', '=', 'test')``` + +##### Filter Mixed ```Seller::whereJoin('city.state.title', '=', 'test')``` -##### Filter sellers which have primary location address = 'test' -```Seller::whereJoin('locationPrimary.address', '=', '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(); +``` -##### Filter sellers which have locationAddress name of primary location = 'test' -```Seller::whereJoin('locationPrimary.locationAddressPrimary.address', '=', 'test')``` +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" +``` -##### You can also combine orderBy more times -```Seller::whereJoin('title', 'test')->whereJoin('city.title', 'test')``` +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 : @@ -298,7 +482,7 @@ $posts = Post::select('posts.*') ->orderBy('categories.name'); if(request()->get('date')){ - $posts->where('posts.date', $date) + $posts->where('date', $date) } $posts = $posts->get(); diff --git a/composer.json b/composer.json index 22f8db6..a5a6e24 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,12 @@ { "name": "fico7489/laravel-eloquent-join", - "description": "This package introduces the join capability for sorting and filtering on eloquent relations.", + "description": "This package introduces the join magic for eloquent models and relations.", "keywords": [ - "laravel eloquent join", + "laravel join", "laravel eloquent join", "laravel sort join", "laravel where join", - "laravel join relation", - "laravel join relations", - "laravel" + "laravel join relation" ], "homepage": "https://github.com/fico7489/laravel-eloquent-join", "support": { diff --git a/src/EloquentJoinBuilder.php b/src/EloquentJoinBuilder.php index 4b99c8e..8854089 100644 --- a/src/EloquentJoinBuilder.php +++ b/src/EloquentJoinBuilder.php @@ -2,25 +2,42 @@ namespace Fico7489\Laravel\EloquentJoin; +use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidAggregateMethod; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelation; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationClause; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationGlobalScope; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationWhere; -use Fico7489\Laravel\EloquentJoin\Relations\HasManyJoin; use Illuminate\Database\Eloquent\Builder; use Fico7489\Laravel\EloquentJoin\Relations\BelongsToJoin; use Fico7489\Laravel\EloquentJoin\Relations\HasOneJoin; +use Fico7489\Laravel\EloquentJoin\Relations\HasManyJoin; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\SoftDeletingScope; class EloquentJoinBuilder extends Builder { - //base builder - public $baseBuilder; + //constants + const AGGREGATE_SUM = 'SUM'; + const AGGREGATE_AVG = 'AVG'; + const AGGREGATE_MAX = 'MAX'; + const AGGREGATE_MIN = 'MIN'; + const AGGREGATE_COUNT = 'COUNT'; //use table alias for join (real table name or uniqid()) private $useTableAlias = false; + //appendRelationsCount + private $appendRelationsCount = false; + + //leftJoin + private $leftJoin = true; + + //aggregate method + private $aggregateMethod = self::AGGREGATE_MAX; + + //base builder + public $baseBuilder; + //store if ->select(...) is already called on builder (we want only one groupBy()) private $selected = false; @@ -63,18 +80,41 @@ public function orWhereJoin($column, $operator, $value) return $this->orWhere($column, $operator, $value); } - public function orderByJoin($column, $direction = 'asc', $leftJoin = true) + public function orderByJoin($column, $direction = 'asc', $aggregateMethod = null) { + $dotPos = strrpos($column, '.'); + $query = $this->baseBuilder ? $this->baseBuilder : $this; - $column = $query->performJoin($column, $leftJoin); + $column = $query->performJoin($column); + if (false !== $dotPos) { + $aggregateMethod = $aggregateMethod ? $aggregateMethod : $this->aggregateMethod; + $this->checkAggregateMethod($aggregateMethod); + $query->selectRaw($aggregateMethod.'('.$column.') as sort'); + + return $this->orderByRaw('sort '.$direction); + } return $this->orderBy($column, $direction); } - public function performJoin($relations, $leftJoin = true) + public function joinRelations($relations, $leftJoin = null) { - $relations = explode('.', $relations); + $leftJoin = null !== $leftJoin ? $leftJoin : $this->leftJoin; + + $query = $this->baseBuilder ? $this->baseBuilder : $this; + $column = $query->performJoin($relations.'.FAKE_FIELD', $leftJoin); + + return $this; + } + private 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(); @@ -84,7 +124,6 @@ public function performJoin($relations, $leftJoin = true) $currentPrimaryKey = $baseModel->getKeyName(); $relationsAccumulated = []; - foreach ($relations as $relation) { if ($relation == $column) { //last item in $relations argument is sort|where column @@ -99,9 +138,13 @@ public function performJoin($relations, $leftJoin = true) $relatedTableAlias = $this->useTableAlias ? uniqid() : $relatedTable; $relationsAccumulated[] = $relatedTableAlias; - $relationAccumulatedString = implode('.', $relationsAccumulated); + $relationAccumulatedString = implode('_', $relationsAccumulated); + + //relations count + if ($this->appendRelationsCount) { + $this->selectRaw('COUNT('.$relatedTable.'.'.$relatedPrimaryKey.') as '.$relationAccumulatedString.'_count'); + } - $joinMethod = $leftJoin ? 'leftJoin' : 'join'; if (!in_array($relationAccumulatedString, $this->joinedTables)) { $joinQuery = $relatedTable.($this->useTableAlias ? ' as '.$relatedTableAlias : ''); if ($relatedRelation instanceof BelongsToJoin) { @@ -112,7 +155,7 @@ public function performJoin($relations, $leftJoin = true) $this->joinQuery($join, $relatedRelation, $relatedTableAlias); }); - } elseif ($relatedRelation instanceof HasOneJoin || $relatedRelation instanceof HasManyJoin) { + } elseif ($relatedRelation instanceof HasOneJoin || $relatedRelation instanceof HasManyJoin) { $relatedKey = $relatedRelation->getQualifiedForeignKeyName(); $relatedKey = last(explode('.', $relatedKey)); @@ -120,18 +163,6 @@ public function performJoin($relations, $leftJoin = true) $join->on($relatedTableAlias.'.'.$relatedKey, '=', $currentTableAlias.'.'.$currentPrimaryKey); $this->joinQuery($join, $relatedRelation, $relatedTableAlias); - - if ($relatedRelation instanceof HasOneJoin) { - - $join->whereRaw( - $relatedTableAlias.'.'.$relatedPrimaryKey.' = ( - SELECT '.$relatedPrimaryKey.' - FROM '.$relatedTableAlias.' - WHERE '.$relatedTableAlias.'.'.$relatedKey.' = '.$currentTableAlias.'.'.$currentPrimaryKey.' - LIMIT 1 - ) - '); - } }); } else { throw new InvalidRelation(); @@ -142,12 +173,13 @@ public function performJoin($relations, $leftJoin = true) $currentTableAlias = $relatedTableAlias; $currentPrimaryKey = $relatedPrimaryKey; - $this->joinedTables[] = implode('.', $relationsAccumulated); + $this->joinedTables[] = implode('_', $relationsAccumulated); } if (!$this->selected && count($relations) > 1) { $this->selected = true; - $this->select($baseTable.'.*'); + $this->selectRaw($baseTable.'.*'); + $this->groupBy($baseTable.'.id'); } return $currentTableAlias.'.'.$column; @@ -206,4 +238,67 @@ private function applyClauseOnRelation($join, $method, $params, $relatedTableAli 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(); + } + } + + //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; + } } diff --git a/src/Exceptions/InvalidAggregateMethod.php b/src/Exceptions/InvalidAggregateMethod.php new file mode 100644 index 0000000..a30f785 --- /dev/null +++ b/src/Exceptions/InvalidAggregateMethod.php @@ -0,0 +1,8 @@ +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; } } diff --git a/src/Traits/ExtendRelationsTrait.php b/src/Traits/ExtendRelationsTrait.php index f7fec67..e55b3c6 100644 --- a/src/Traits/ExtendRelationsTrait.php +++ b/src/Traits/ExtendRelationsTrait.php @@ -5,69 +5,24 @@ use Fico7489\Laravel\EloquentJoin\Relations\BelongsToJoin; use Fico7489\Laravel\EloquentJoin\Relations\HasManyJoin; use Fico7489\Laravel\EloquentJoin\Relations\HasOneJoin; -use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Builder; -/** - * Add new realations BelongsTo and HasOne. - */ trait ExtendRelationsTrait { - /** - * Define an inverse one-to-one or many relationship. - * - * @param string $related - * @param string $foreignKey - * @param string $ownerKey - * @param string $relation - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function belongsTo($related, $foreignKey = null, $ownerKey = null, $relation = null) + protected function newBelongsTo(Builder $query, Model $child, $foreignKey, $ownerKey, $relation) { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relationships. - if (is_null($relation)) { - $relation = $this->guessBelongsToRelation(); - } - - $instance = $this->newRelatedInstance($related); - - // If no foreign key was supplied, we can use a backtrace to guess the proper - // foreign key name by using the name of the relationship function, which - // when combined with an "_id" should conventionally match the columns. - if (is_null($foreignKey)) { - $foreignKey = Str::snake($relation).'_'.$instance->getKeyName(); - } - - // Once we have the foreign key names, we'll just create a new Eloquent query - // for the related models and returns the relationship instance which will - // actually be responsible for retrieving and hydrating every relations. - $ownerKey = $ownerKey ?: $instance->getKeyName(); - - return new BelongsToJoin( - $instance->newQuery(), $this, $foreignKey, $ownerKey, $relation - ); + return new BelongsToJoin($query, $child, $foreignKey, $ownerKey, $relation); } - /** - * Define a one-to-one relationship. - * - * @param string $related - * @param string $foreignKey - * @param string $localKey - * - * @return \Illuminate\Database\Eloquent\Relations\HasOne - */ - public function hasOne($related, $foreignKey = null, $localKey = null) + protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey) { - $instance = $this->newRelatedInstance($related); - - $foreignKey = $foreignKey ?: $this->getForeignKey(); - - $localKey = $localKey ?: $this->getKeyName(); + return new HasOneJoin($query, $parent, $foreignKey, $localKey); + } - return new HasOneJoin($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey); + protected function newHasMany(Builder $query, Model $parent, $foreignKey, $localKey) + { + return new HasManyJoin($query, $parent, $foreignKey, $localKey); } /** diff --git a/tests/Models/City.php b/tests/Models/City.php index 51367db..513d5c4 100644 --- a/tests/Models/City.php +++ b/tests/Models/City.php @@ -22,4 +22,14 @@ 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); + } } diff --git a/tests/Models/Integration.php b/tests/Models/Integration.php new file mode 100644 index 0000000..416195f --- /dev/null +++ b/tests/Models/Integration.php @@ -0,0 +1,14 @@ +hasOne(LocationAddress::class) ->where('is_primary', '=', 1); } + + public function integrations() + { + return $this->hasMany(Integration::class); + } } diff --git a/tests/Models/LocationAddress.php b/tests/Models/LocationAddress.php index ec38dd4..cdd5644 100644 --- a/tests/Models/LocationAddress.php +++ b/tests/Models/LocationAddress.php @@ -11,4 +11,9 @@ class LocationAddress extends BaseModel protected $table = 'location_addresses'; protected $fillable = ['address', 'is_primary']; + + public function users() + { + return $this->hasMany(User::class); + } } diff --git a/tests/Models/User.php b/tests/Models/User.php new file mode 100644 index 0000000..1bcdaf1 --- /dev/null +++ b/tests/Models/User.php @@ -0,0 +1,14 @@ + 'testbench']); + $_ENV['type'] = 'sqlite'; //sqlite, mysql, pgsql + + \Artisan::call('migrate', ['--database' => $_ENV['type']]); + $migrator = $this->app->make('migrator'); $migrator->run($path); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 7532277..28f3931 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,7 +16,7 @@ public function setUp() $seller = Seller::create(['title' => 1]); $seller2 = Seller::create(['title' => 2]); $seller3 = Seller::create(['title' => 3]); - Seller::create(['title' => 4]); + $seller4 = Seller::create(['title' => 4]); Location::create(['address' => 1, 'seller_id' => $seller->id]); Location::create(['address' => 2, 'seller_id' => $seller2->id]); @@ -26,13 +26,13 @@ public function setUp() Location::create(['address' => 4, 'seller_id' => $seller3->id, 'is_primary' => 1]); Location::create(['address' => 5, 'seller_id' => $seller3->id, 'is_secondary' => 1]); - Order::create(['number' => '1', 'seller_id' => $seller->id]); - Order::create(['number' => '2', 'seller_id' => $seller2->id]); - Order::create(['number' => '3', 'seller_id' => $seller3->id]); + $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' => $seller->id]); - OrderItem::create(['name' => '2', 'order_id' => $seller2->id]); - OrderItem::create(['name' => '3', 'order_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(); } @@ -52,12 +52,37 @@ protected function fetchQuery() protected function getEnvironmentSetUp($app) { // Setup default database to use sqlite :memory: - $app['config']->set('database.default', 'testbench'); - $app['config']->set('database.connections.testbench', [ + $app['config']->set('database.default', 'sqlite'); + $app['config']->set('database.connections.sqlite', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); + + $app['config']->set('database.default', 'mysql'); + $app['config']->set('database.connections.mysql', [ + 'driver' => 'mysql', + 'host' => 'localhost', + 'database' => 'join', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'strict' => false, + ]); + + $app['config']->set('database.default', 'pgsql'); + $app['config']->set('database.connections.pgsql', [ + 'driver' => 'pgsql', + 'host' => 'localhost', + 'database' => 'join', + 'username' => 'postgres', + 'password' => 'root', + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + 'sslmode' => 'prefer', + ]); } protected function getPackageProviders($app) @@ -74,6 +99,9 @@ protected function assertQueryMatches($expected, $actual) $expected = str_replace(['\n', '\r'], '', $expected); $expected = '/'.$expected.'/'; $expected = preg_quote($expected); + if ('mysql' == $_ENV['type']) { + $expected = str_replace(['"'], '`', $expected); + } $this->assertRegExp($expected, $actual); } diff --git a/tests/Tests/AggregateJoinTest.php b/tests/Tests/AggregateJoinTest.php new file mode 100644 index 0000000..2510181 --- /dev/null +++ b/tests/Tests/AggregateJoinTest.php @@ -0,0 +1,67 @@ +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()); + } +} diff --git a/tests/Tests/AppendRelationsCountTest.php b/tests/Tests/AppendRelationsCountTest.php new file mode 100644 index 0000000..c5c08f8 --- /dev/null +++ b/tests/Tests/AppendRelationsCountTest.php @@ -0,0 +1,28 @@ +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" = ? + and "locations"."deleted_at" is null + left join "location_addresses" on "location_addresses"."location_id" = "locations"."id" + and "location_addresses"."is_primary" = ? + and "location_addresses"."deleted_at" is null + where "orders"."deleted_at" is null + group by "orders"."id"'; + + $this->assertQueryMatches($queryTest, $this->fetchQuery()); + } +} diff --git a/tests/Tests/BelongsToLeftJoinTest.php b/tests/Tests/BelongsToLeftJoinTest.php deleted file mode 100644 index 319f65f..0000000 --- a/tests/Tests/BelongsToLeftJoinTest.php +++ /dev/null @@ -1,25 +0,0 @@ -', 0)->forceDelete(); - City::where('id', '>', 0)->forceDelete(); - - $items = Seller::orderByJoin('city.name')->get(); - $this->assertEquals(0, $items->count()); - - $seller = Seller::create(['title' => 'test']); - - $items = Seller::orderByJoin('city.name')->get(); - $this->assertEquals(1, $items->count()); - $this->assertEquals(1, Seller::count()); - } -} diff --git a/tests/Tests/Clauses/JoinRelationsTest.php b/tests/Tests/Clauses/JoinRelationsTest.php new file mode 100644 index 0000000..b0eb5e1 --- /dev/null +++ b/tests/Tests/Clauses/JoinRelationsTest.php @@ -0,0 +1,23 @@ +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()); + } +} diff --git a/tests/Tests/Clauses/OrWhereTest.php b/tests/Tests/Clauses/OrWhereTest.php new file mode 100644 index 0000000..68f98b3 --- /dev/null +++ b/tests/Tests/Clauses/OrWhereTest.php @@ -0,0 +1,26 @@ +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" = ? or "sellers"."id" = ?) + and "orders"."deleted_at" is null + group by "orders"."id"'; + + $this->assertQueryMatches($queryTest, $this->fetchQuery()); + } +} diff --git a/tests/Tests/Clauses/OrderByTest.php b/tests/Tests/Clauses/OrderByTest.php new file mode 100644 index 0000000..0cb7510 --- /dev/null +++ b/tests/Tests/Clauses/OrderByTest.php @@ -0,0 +1,25 @@ +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()); + } +} diff --git a/tests/Tests/Clauses/WhereTest.php b/tests/Tests/Clauses/WhereTest.php new file mode 100644 index 0000000..bc7ada4 --- /dev/null +++ b/tests/Tests/Clauses/WhereTest.php @@ -0,0 +1,25 @@ +whereJoin('seller.id', '=', 1) + ->get(); + + $queryTest = '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"'; + + $this->assertQueryMatches($queryTest, $this->fetchQuery()); + } +} diff --git a/tests/Tests/WhereOnRelationTest.php b/tests/Tests/ClosureOnRelationTest.php similarity index 70% rename from tests/Tests/WhereOnRelationTest.php rename to tests/Tests/ClosureOnRelationTest.php index 662cd9a..1a51a19 100644 --- a/tests/Tests/WhereOnRelationTest.php +++ b/tests/Tests/ClosureOnRelationTest.php @@ -5,77 +5,57 @@ use Fico7489\Laravel\EloquentJoin\Tests\Models\Seller; use Fico7489\Laravel\EloquentJoin\Tests\TestCase; -class WhereOnRelationTest extends TestCase +class ClosureOnRelationTest extends TestCase { public function testWhereOnRelationWithOrderByJoin() { //location have two where ['is_primary => 0', 'is_secondary' => 0] $items = Seller::orderByJoin('location.id', 'desc')->get(); - $queryTest = 'select "sellers".* from "sellers" + $queryTest = 'select sellers.*, MAX(locations.id) as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = ? and "locations"."is_secondary" = ? and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - order by "locations"."id" desc'; + 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".* from "sellers" + $queryTest = 'select sellers.*, MAX(locations.id) as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = ? and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - order by "locations"."id" desc'; + 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".* from "sellers" + $queryTest = 'select sellers.*, MAX(locations.id) as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_secondary" = ? and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - order by "locations"."id" desc'; + 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".* from "sellers" + $queryTest = 'select sellers.*, MAX(locations.id) as sort from "sellers" left join "locations" on "locations"."seller_id" = "sellers"."id" and "locations"."is_primary" = ? or "locations"."is_secondary" = ? and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - order by "locations"."id" desc'; + group by "sellers"."id" + order by sort desc'; $this->assertQueryMatches($queryTest, $this->fetchQuery()); } diff --git a/tests/Tests/ClosureTest.php b/tests/Tests/ClosureTest.php index d08bd46..716cb77 100644 --- a/tests/Tests/ClosureTest.php +++ b/tests/Tests/ClosureTest.php @@ -15,7 +15,7 @@ public function testNestOne() ->orWhereJoin('order.id', '=', 2); })->get(); - $queryTest = 'select "order_items".* + $queryTest = 'select order_items.* from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null @@ -36,19 +36,15 @@ public function testNestTwo() }); })->get(); - $queryTest = 'select "order_items".* from "order_items" + $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" = ? and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) where ("orders"."id" = ? or "orders"."id" = ? + where ("orders"."id" = ? or "orders"."id" = ? and ("locations"."id" = ?)) and "order_items"."deleted_at" is null'; diff --git a/tests/Tests/ExceptionTest.php b/tests/Tests/ExceptionTest.php index 958fa8b..1211af7 100644 --- a/tests/Tests/ExceptionTest.php +++ b/tests/Tests/ExceptionTest.php @@ -2,10 +2,12 @@ namespace Fico7489\Laravel\EloquentJoin\Tests\Tests; +use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidAggregateMethod; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelation; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationClause; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationGlobalScope; use Fico7489\Laravel\EloquentJoin\Exceptions\InvalidRelationWhere; +use Fico7489\Laravel\EloquentJoin\Tests\Models\City; use Fico7489\Laravel\EloquentJoin\Tests\Models\Seller; use Fico7489\Laravel\EloquentJoin\Tests\TestCase; @@ -14,7 +16,7 @@ class ExceptionTest extends TestCase public function testInvalidRelation() { try { - Seller::whereJoin('locations.address', '=', 'test')->get(); + City::whereJoin('sellers.id', '=', 'test')->get(); } catch (InvalidRelation $e) { $this->assertEquals((new InvalidRelation())->message, $e->getMessage()); @@ -62,4 +64,17 @@ public function testInvalidRelationGlobalScope() $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); + } } diff --git a/tests/Tests/HasOneLeftJoinTest.php b/tests/Tests/HasOneLeftJoinTest.php deleted file mode 100644 index e56642d..0000000 --- a/tests/Tests/HasOneLeftJoinTest.php +++ /dev/null @@ -1,42 +0,0 @@ -', 0)->forceDelete(); - Location::where('id', '>', 0)->forceDelete(); - - $items = Seller::orderByJoin('location.address')->get(); - $this->assertEquals(0, $items->count()); - - $seller = Seller::create(['title' => 'test']); - Location::create(['address' => 'test', 'seller_id' => $seller->id, 'is_primary' => 1]); - Location::create(['address' => 'test2', 'seller_id' => $seller->id, 'is_primary' => 1]); - - $items = Seller::orderByJoin('locationPrimary.address')->get(); - $this->assertEquals(1, $items->count()); - $this->assertEquals(1, Seller::count()); - } - - public function testLeftJoinEmpty() - { - Seller::where('id', '>', 0)->forceDelete(); - Location::where('id', '>', 0)->forceDelete(); - - $items = Seller::orderByJoin('location.address')->get(); - $this->assertEquals(0, $items->count()); - - $seller = Seller::create(['title' => 'test']); - - $items = Seller::orderByJoin('locationPrimary.address')->get(); - $this->assertEquals(1, $items->count()); - $this->assertEquals(1, Seller::count()); - } -} diff --git a/tests/Tests/JoinTypeTest.php b/tests/Tests/JoinTypeTest.php new file mode 100644 index 0000000..e17bf60 --- /dev/null +++ b/tests/Tests/JoinTypeTest.php @@ -0,0 +1,55 @@ +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" = ?'; + + $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" = ?'; + + $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()); + } +} diff --git a/tests/Tests/OptionsTest.php b/tests/Tests/OptionsTest.php new file mode 100644 index 0000000..4253d27 --- /dev/null +++ b/tests/Tests/OptionsTest.php @@ -0,0 +1,42 @@ +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()); + } +} diff --git a/tests/Tests/OrderByJoinTest.php b/tests/Tests/OrderByJoinTest.php deleted file mode 100644 index 71e3002..0000000 --- a/tests/Tests/OrderByJoinTest.php +++ /dev/null @@ -1,83 +0,0 @@ -assertEquals($order[0], $items->get(0)->id); - $this->assertEquals($order[1], $items->get(1)->id); - $this->assertEquals($order[2], $items->get(2)->id); - $this->assertEquals($count, $items->count()); - } - - public function testOrderByJoinWithoutJoining() - { - $items = OrderItem::orderByJoin('name')->get(); - $this->checkOrder($items, [1, 2, 3], 3); - - OrderItem::find(2)->update(['name' => 9]); - $items = OrderItem::orderByJoin('name')->get(); - $this->checkOrder($items, [1, 3, 2], 3); - - $items = OrderItem::orderByJoin('name', 'desc')->get(); - $this->checkOrder($items, [2, 3, 1], 3); - } - - public function testOrderByJoinJoinFirstRelation() - { - $items = OrderItem::orderByJoin('order.number')->get(); - $this->checkOrder($items, [1, 2, 3], 3); - - Order::find(2)->update(['number' => 9]); - $items = OrderItem::orderByJoin('order.number')->get(); - $this->checkOrder($items, [1, 3, 2], 3); - - $items = OrderItem::orderByJoin('order.number', 'desc')->get(); - $this->checkOrder($items, [2, 3, 1], 3); - - OrderItem::create(['name' => '4', 'order_id' => null]); - $items = OrderItem::orderByJoin('order.number', 'desc')->get(); - $this->checkOrder($items, [2, 3, 1], 4); - - $items = OrderItem::orderByJoin('order.number', 'asc')->get(); - $this->checkOrder($items, [4, 1, 3], 4); - } - - public function testOrderByJoinJoinSecondRelation() - { - $items = OrderItem::orderByJoin('order.seller.title')->get(); - $this->checkOrder($items, [1, 2, 3], 3); - - $items = OrderItem::orderByJoin('order.seller.title', 'desc')->get(); - $this->checkOrder($items, [3, 2, 1], 3); - - Seller::find(2)->update(['title' => 9]); - $items = OrderItem::orderByJoin('order.seller.title', 'desc')->get(); - $this->checkOrder($items, [2, 3, 1], 3); - - OrderItem::create(['name' => '4', 'order_id' => null]); - $items = OrderItem::orderByJoin('order.seller.title', 'asc')->get(); - $this->checkOrder($items, [4, 1, 3], 4); - } - - public function testOrderByJoinJoinThirdRelationHasOne() - { - $items = OrderItem::orderByJoin('order.seller.location.address')->get(); - $this->checkOrder($items, [1, 2, 3], 3); - - $items = OrderItem::orderByJoin('order.seller.location.address', 'desc')->get(); - $this->checkOrder($items, [3, 2, 1], 3); - - Location::find(2)->update(['address' => 9]); - $items = OrderItem::orderByJoin('order.seller.location.address', 'desc')->get(); - $this->checkOrder($items, [2, 3, 1], 3); - } -} diff --git a/tests/Tests/Relations/BelongsToTest.php b/tests/Tests/Relations/BelongsToTest.php new file mode 100644 index 0000000..4fdf95c --- /dev/null +++ b/tests/Tests/Relations/BelongsToTest.php @@ -0,0 +1,84 @@ +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" = ? + 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" = ? 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" = ? + and "locations"."deleted_at" is null + left join "location_addresses" on "location_addresses"."location_id" = "locations"."id" + and "location_addresses"."is_primary" = ? + and "location_addresses"."deleted_at" is null + where "orders"."deleted_at" is null + group by "orders"."id"'; + + $this->assertQueryMatches($queryTest, $this->fetchQuery()); + } +} diff --git a/tests/Tests/Relations/HasManyTest.php b/tests/Tests/Relations/HasManyTest.php new file mode 100644 index 0000000..3912178 --- /dev/null +++ b/tests/Tests/Relations/HasManyTest.php @@ -0,0 +1,51 @@ +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()); + } +} diff --git a/tests/Tests/Relations/HasOneTest.php b/tests/Tests/Relations/HasOneTest.php new file mode 100644 index 0000000..fa9bf78 --- /dev/null +++ b/tests/Tests/Relations/HasOneTest.php @@ -0,0 +1,57 @@ +get(); + + $queryTest = 'select sellers.* + from "sellers" + left join "locations" on "locations"."seller_id" = "sellers"."id" + and "locations"."is_primary" = ? + and "locations"."is_secondary" = ? + 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" = ? + and "locations"."is_secondary" = ? + 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" = ? + and "locations"."is_secondary" = ? + 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()); + } +} diff --git a/tests/Tests/SoftDeleteTest.php b/tests/Tests/SoftDeleteTest.php index 49e0087..aeff8e6 100644 --- a/tests/Tests/SoftDeleteTest.php +++ b/tests/Tests/SoftDeleteTest.php @@ -10,70 +10,130 @@ class SoftDeleteTest extends TestCase public function testNotRelatedWithoutTrashedDefault() { OrderItem::orderByJoin('name')->get(); - $queryTest = '/select \* from "order_items" where "order_items"."deleted_at" is null order by "order_items"."name" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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 testNotRelatedWithoutTrashed() + 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->assertRegExp($queryTest, $this->fetchQuery()); + $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 testNotRelatedOnlyTrashed() + 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->assertRegExp($queryTest, $this->fetchQuery()); + $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 testNotRelatedWithTrashed() + public function testNotRelatedWithTrashedExplicit() { OrderItem::orderByJoin('name')->withTrashed()->get(); - $queryTest = '/select \* from "order_items" order by "order_items"."name" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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".* 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 order by "orders"."number" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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 testRelatedWithoutTrashed() + public function testRelatedWithoutTrashedExplicit() { OrderItem::orderByJoin('order.number')->withoutTrashed()->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 "order_items"."deleted_at" is null order by "orders"."number" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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 testRelatedOnlyTrashed() + public function testRelatedOnlyTrashedExplicit() { OrderItem::orderByJoin('order.number')->onlyTrashed()->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 "order_items"."deleted_at" is not null order by "orders"."number" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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 testRelatedWithTrashed() + public function testRelatedWithTrashedExplicit() { OrderItem::orderByJoin('order.number')->withTrashed()->get(); - $queryTest = '/select "order_items".* from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" and "orders"."deleted_at" is null order by "orders"."number" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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".* from "order_items" left join "orders" on "orders"."id" = "order_items"."order_id" where "order_items"."deleted_at" is null order by "orders"."number" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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".* 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 order by "orders"."number" asc/'; - $this->assertRegExp($queryTest, $this->fetchQuery()); + $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()); } } diff --git a/tests/Tests/WhereJoinTest.php b/tests/Tests/WhereJoinTest.php deleted file mode 100644 index 9c1e786..0000000 --- a/tests/Tests/WhereJoinTest.php +++ /dev/null @@ -1,177 +0,0 @@ -get(); - - $queryTest = 'select * from "sellers" - where "sellers"."title" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinBelongsTo() - { - Seller::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" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinHasOne() - { - Seller::whereJoin('locationPrimary.address', '=', 'test')->get(); - - $queryTest = 'select "sellers".* from "sellers" - left join "locations" - on "locations"."seller_id" = "sellers"."id" - and "locations"."is_primary" = ? - and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - where "locations"."address" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinBelongsToBelongsTo() - { - Seller::whereJoin('city.state.name', '=', 'test')->get(); - - $queryTest = 'select "sellers".* from "sellers" - left 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 "states"."name" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinBelongsToHasOne() - { - Seller::whereJoin('city.zipCodePrimary.name', '=', 'test')->get(); - - $queryTest = 'select "sellers".* from "sellers" - left join "cities" - on "cities"."id" = "sellers"."city_id" - and "cities"."deleted_at" is null - left join "zip_codes" - on "zip_codes"."city_id" = "cities"."id" - and "zip_codes"."is_primary" = ? - and "zip_codes"."deleted_at" is null - and zip_codes.id = ( - SELECT id - FROM zip_codes - WHERE zip_codes.city_id = cities.id - LIMIT 1 - ) - where "zip_codes"."name" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinHasOneHasOne() - { - Seller::whereJoin('locationPrimary.locationAddressPrimary.name', '=', 'test')->get(); - - $queryTest = 'select "sellers".* from "sellers" - left join "locations" - on "locations"."seller_id" = "sellers"."id" - and "locations"."is_primary" = ? - and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - left join "location_addresses" - on "location_addresses"."location_id" = "locations"."id" - and "location_addresses"."is_primary" = ? - and "location_addresses"."deleted_at" is null - and location_addresses.id = ( - SELECT id - FROM location_addresses - WHERE location_addresses.location_id = locations.id - LIMIT 1 - ) - where "location_addresses"."name" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinHasBelongsTo() - { - Seller::whereJoin('locationPrimary.city.name', '=', 'test')->get(); - - $queryTest = 'select "sellers".* from "sellers" - left join "locations" - on "locations"."seller_id" = "sellers"."id" - and "locations"."is_primary" = ? - and "locations"."deleted_at" is null - and locations.id = ( - SELECT id - FROM locations - WHERE locations.seller_id = sellers.id - LIMIT 1 - ) - left join "cities" - on "cities"."id" = "locations"."city_id" - and "cities"."deleted_at" is null - where "cities"."name" = ?'; - - $this->assertQueryMatches($queryTest, $this->fetchQuery()); - } - - public function testWhereJoinGeneral() - { - Order::find(1)->update(['number' => 'aaaa']); - Order::find(2)->update(['number' => 'bbbb']); - Order::find(3)->update(['number' => 'cccc']); - - //test where does not exists - $items = OrderItem::orderByJoin('order.number')->whereJoin('order.number', '=', 'dddd')->get(); - $this->assertEquals(0, $items->count()); - - //test where does exists - $items = OrderItem::orderByJoin('order.number')->whereJoin('order.number', '=', 'cccc')->get(); - $this->assertEquals(1, $items->count()); - - //test where does exists, without orderByJoin - $items = OrderItem::whereJoin('order.number', '=', 'cccc')->get(); - $this->assertEquals(1, $items->count()); - - //test more where does not exists - $items = OrderItem::orderByJoin('order.number')->whereJoin('order.number', '=', 'bbbb')->whereJoin('order.number', '=', 'cccc')->get(); - $this->assertEquals(0, $items->count()); - - //test more where with orWhere exists - $items = OrderItem::orderByJoin('order.number')->whereJoin('order.number', '=', 'bbbb')->orWhereJoin('order.number', '=', 'cccc')->get(); - $this->assertEquals(2, $items->count()); - - //test more where with orWhere does not exists - $items = OrderItem::orderByJoin('order.number')->whereJoin('order.number', '=', 'dddd')->orWhereJoin('order.number', '=', 'eeee')->get(); - $this->assertEquals(0, $items->count()); - } -} diff --git a/tests/database/migrations/2017_11_04_163552_create_database.php b/tests/database/migrations/2017_11_04_163552_create_database.php index 473109b..d7a4813 100644 --- a/tests/database/migrations/2017_11_04_163552_create_database.php +++ b/tests/database/migrations/2017_11_04_163552_create_database.php @@ -103,6 +103,28 @@ public function up() $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(); + }); } /** @@ -118,5 +140,6 @@ public function down() Schema::drop('zip_codes'); Schema::drop('states'); Schema::drop('location_addresses'); + Schema::drop('integrations'); } }