From d53deaf630337e2b32675fa2d8472e9234bcc509 Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 11:29:55 +0200 Subject: [PATCH 01/39] added one-of-many to has-one --- .../Database/Eloquent/PartialRelation.php | 29 +++ .../Concerns/QueriesRelationships.php | 19 +- .../Relations/Concerns/CanBeOneOfMany.php | 214 ++++++++++++++++++ .../Concerns/ComparesRelatedModels.php | 25 +- .../Database/Eloquent/Relations/HasOne.php | 68 +++++- .../DatabaseEloquentHasOneOfManyTest.php | 199 ++++++++++++++++ .../Database/EloquentHasOneOfManyTest.php | 77 +++++++ 7 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php create mode 100644 src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php create mode 100755 tests/Database/DatabaseEloquentHasOneOfManyTest.php create mode 100644 tests/Integration/Database/EloquentHasOneOfManyTest.php diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php new file mode 100644 index 000000000000..baf94e6ac079 --- /dev/null +++ b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php @@ -0,0 +1,29 @@ +orders = null; $query->setBindings([], 'order'); - if (count($query->columns) > 1) { + if (count($query->columns) > 1 && !$this->isOneOfMany($relation)) { $query->columns = [$query->columns[0]]; $query->bindings['select'] = []; } @@ -432,6 +433,18 @@ public function withAggregate($relations, $column, $function = null) return $this; } + /** + * Determines whether the given relation is one-of-many. + * + * @param \Illuminate\Database\Eloquent\Relations\Relation $relation + * @return boolean + */ + protected function isOneOfMany($relation) + { + return $relation instanceof PartialRelation + && $relation->isOneOfMany(); + } + /** * Add subselect queries to count the relations. * @@ -503,7 +516,9 @@ public function withAvg($relation, $column) */ protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean) { - $hasQuery->mergeConstraintsFrom($relation->getQuery()); + if(!$this->isOneOfMany($relation)) { + $hasQuery->mergeConstraintsFrom($relation->getQuery()); + } return $this->canUseExistsForExistenceCheck($operator, $count) ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php new file mode 100644 index 000000000000..4840ad55c2fb --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -0,0 +1,214 @@ +isOneOfMany = true; + + if ($ofMany) { + $this->setOneOfManyQuery(); + } + + return $this; + } + + /** + * Initially set one-of-many parent query. + * + * @return void + */ + protected function setOneOfManyQuery() + { + if ($this->oneOfManyQuery) { + return; + } + + $this->oneOfManyQuery = $this->getOneOfManyQueryFor($this->query); + } + + /** + * Get related key name. + * + * @return string + */ + protected function getRelatedKeyName() + { + return $this->getRelatedTableName().'.'.$this->query->getModel()->getKeyName(); + } + + /** + * Get sub select alias. + * + * @return string + */ + protected function getSubSelectAlias() + { + return $this->getRelatedTableName()."_{$this->localKey}_".spl_object_id($this); + } + + /** + * Determines wether the relationship is one-of-many. + * + * @return boolean + */ + public function isOneOfMany() + { + return $this->isOneOfMany; + } + + /** + * Add subselect contstraints to the given query builder. + * + * @param Builder $query + * @return void + */ + protected function addSubSelectConstraintsTo(Builder $query) + { + $query + ->from($this->getRelatedTableName(), $this->getSubSelectTableAlias()) + ->whereColumn($this->qualifySubSelectColumn($this->foreignKey), $this->foreignKey) + ->select($this->qualifySubSelectColumn($query->getModel()->getKeyName())) + ->take(1); + } + + /** + * Get the subselect table alias. + * + * @return string + */ + protected function getSubSelectTableAlias() + { + return $this->getRelatedTableName().'_'.spl_object_id($this); + } + + /** + * Get the qualified subselect column name. + * + * @param string $column + * @return string + */ + protected function qualifySubSelectColumn($column) + { + $segments = explode('.', $column); + + return $this->getSubSelectTableAlias().'.'.end($segments); + } + + /** + * Get the related table name. + * + * @return string + */ + protected function getRelatedTableName() + { + return $this->query->getModel()->getTable(); + } + + /** + * Get the result query builder instance for the given query builder. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function getOneOfManyQueryFor(Builder $query) + { + return $query->clone(); + } + + /** + * Resolve the one-of-many query. + * + * @param \Illuminate\Database\Eloquent\Builder|null $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function resolveOneOfManyQuery(Builder $query = null) + { + if (is_null($query)) { + $query = $this->query; + } + + $this->addSubSelectConstraintsTo($query); + + return $this->oneOfManyQuery + ->whereExists(function ($existsQuery) use ($query) { + $existsQuery + ->selectSub($query, $this->getSubSelectAlias()) + ->whereColumn($this->getSubSelectAlias(), $this->getRelatedKeyName()); + }); + } + + /** + * Determines wether the given query method should be forwarded to the + * one-of-many query. + * + * @param string $method + * @return bool + */ + protected function shouldForwardedToOneOfManyQuery($method) + { + return $this->isOneOfMany() + && in_array($method, $this->forwardToOneOfManyQuery); + } + + /** + * Handle dynamic method calls to the relationship. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); + } + + $query = $this->query; + if($this->shouldForwardedToOneOfManyQuery($method)) { + $query = $this->resolveOneOfManyQuery(); + } + + $result = $this->forwardCallTo($query, $method, $parameters); + + if ($result === $query) { + return $this; + } + + return $result; + } +} diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php index 50ec4f03e337..6e1fa55cfffd 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; +use Illuminate\Contracts\Database\Eloquent\PartialRelation; use Illuminate\Database\Eloquent\Model; trait ComparesRelatedModels @@ -17,7 +18,8 @@ public function is($model) return ! is_null($model) && $this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) && $this->related->getTable() === $model->getTable() && - $this->related->getConnectionName() === $model->getConnectionName(); + $this->related->getConnectionName() === $model->getConnectionName() && + $this->compareOneOfMany($model); } /** @@ -65,4 +67,25 @@ protected function compareKeys($parentKey, $relatedKey) return $parentKey === $relatedKey; } + + /** + * Determine if the given model is the correct relationship model. + * + * @param \Illuminate\Database\Eloquent\Model|null $model + * @return bool + */ + protected function compareOneOfMany($model) + { + if (! $this instanceof PartialRelation) { + return true; + } + + if(! $this->isOneOfMany()) { + return true; + } + + return $this->resolveOneOfManyQuery() + ->whereKey($model->getKey()) + ->exists(); + } } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index 81ca9bb441cf..b8cf746b0c74 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -2,14 +2,17 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Contracts\Database\Eloquent\PartialRelation; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany; use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; -class HasOne extends HasOneOrMany +class HasOne extends HasOneOrMany implements PartialRelation { - use ComparesRelatedModels, SupportsDefaultModels; + use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels; /** * Get the results of the relationship. @@ -77,4 +80,65 @@ protected function getRelatedKeyFrom(Model $model) { return $model->getAttribute($this->getForeignKeyName()); } + + /** + * Set the constraints for an eager load of the relation. + * + * @param array $models + * @return void + */ + public function addEagerConstraints(array $models) + { + if(! $this->isOneOfMany()) { + return parent::addEagerConstraints($models); + } + + $whereIn = $this->whereInMethod($this->parent, $this->localKey); + + $this->oneOfManyQuery->{$whereIn}( + $this->foreignKey, $this->getKeys($models, $this->localKey) + ); + } + + /** + * Add the constraints for an internal relationship existence query. + * + * Essentially, these queries compare on column names like whereColumn. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) + { + if(! $this->isOneOfMany()) { + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + } + + $query->whereColumn( + $this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey() + ); + + $query->getQuery()->orders = $this->query->getQuery()->orders; + + $this->oneOfManyQuery->select($columns); + + return $this->resolveOneOfManyQuery($query); + } + + /** + * Execute the query as a "select" statement. + * + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function get($columns = ['*']) + { + if (! $this->isOneOfMany()) { + return parent::get($columns); + } + + return $this->resolveOneOfManyQuery()->get($columns); + } } diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php new file mode 100755 index 000000000000..8a87f8c933b7 --- /dev/null +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -0,0 +1,199 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + + $this->createSchema(); + } + + /** + * Setup the database schema. + * + * @return void + */ + public function createSchema() + { + $this->schema()->create('users', function ($table) { + $table->increments('id'); + }); + + $this->schema()->create('logins', function ($table) { + $table->increments('id'); + $table->foreignId('user_id'); + }); + } + + /** + * Tear down the database schema. + * + * @return void + */ + protected function tearDown(): void + { + $this->schema()->drop('users'); + $this->schema()->drop('logins'); + } + + public function testItEagerLoadsCorrectModels() + { + $user = HasOneOfManyTestUser::create(); + $user->logins()->create(); + $latestLogin = $user->logins()->create(); + + $user = HasOneOfManyTestUser::with('latest_login')->first(); + + $this->assertTrue($user->relationLoaded('latest_login')); + $this->assertSame($latestLogin->id, $user->latest_login->id); + } + + + public function testHasNested() + { + $user = HasOneOfManyTestUser::create(); + $previousLogin = $user->logins()->create(); + $latestLogin = $user->logins()->create(); + + $found = HasOneOfManyTestUser::whereHas('latest_login', function ($query) use ($latestLogin) { + $query->where('id', $latestLogin->id); + })->exists(); + $this->assertTrue($found); + + $found = HasOneOfManyTestUser::whereHas('latest_login', function ($query) use ($previousLogin) { + $query->where('id', $previousLogin->id); + })->exists(); + $this->assertFalse($found); + } + + public function testHasCount() + { + $user = HasOneOfManyTestUser::create(); + $user->logins()->create(); + $user->logins()->create(); + + $user = HasOneOfManyTestUser::withCount('latest_login')->first(); + $this->assertEquals(1, $user->latest_login_count); + } + + public function testExists() + { + $user = HasOneOfManyTestUser::create(); + $previousLogin = $user->logins()->create(); + $latestLogin = $user->logins()->create(); + + $this->assertFalse($user->latest_login()->whereKey($previousLogin->getKey())->exists()); + $this->assertTrue($user->latest_login()->whereKey($latestLogin->getKey())->exists()); + } + + public function testIsMethod() + { + $user = HasOneOfManyTestUser::create(); + $login1 = $user->latest_login()->create(); + $login2 = $user->latest_login()->create(); + + $this->assertFalse($user->latest_login()->is($login1)); + $this->assertTrue($user->latest_login()->is($login2)); + } + + public function testIsNotMethod() + { + $user = HasOneOfManyTestUser::create(); + $login1 = $user->latest_login()->create(); + $login2 = $user->latest_login()->create(); + + $this->assertTrue($user->latest_login()->isNot($login1)); + $this->assertFalse($user->latest_login()->isNot($login2)); + } + + /** + * @group fail + */ + public function testGet() + { + $user = HasOneOfManyTestUser::create(); + $previousLogin = $user->logins()->create(); + $latestLogin = $user->logins()->create(); + + $latestLogins = $user->latest_login()->get(); + $this->assertCount(1, $latestLogins); + $this->assertSame($latestLogin->id, $latestLogins->first()->id); + + $latestLogins = $user->latest_login()->whereKey($previousLogin->getKey())->get(); + $this->assertCount(0, $latestLogins); + } + + public function testCount() + { + $user = HasOneOfManyTestUser::create(); + $user->logins()->create(); + $user->logins()->create(); + + $this->assertSame(1, $user->latest_login()->count()); + } + + /** + * Get a database connection instance. + * + * @return \Illuminate\Database\Connection + */ + protected function connection() + { + return Eloquent::getConnectionResolver()->connection(); + } + + /** + * Get a schema builder instance. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function schema() + { + return $this->connection()->getSchemaBuilder(); + } +} + +/** + * Eloquent Models... + */ +class HasOneOfManyTestUser extends Eloquent +{ + protected $table = 'users'; + protected $guarded = []; + public $timestamps = false; + + public function logins() + { + return $this->hasMany(HasOneOfManyTestLogin::class, 'user_id'); + } + + public function latest_login() + { + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany()->orderByDesc('id'); + } +} + +class HasOneOfManyTestLogin extends Eloquent +{ + protected $table = 'logins'; + protected $guarded = []; + public $timestamps = false; +} diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php new file mode 100644 index 000000000000..1e7a2cb9ce77 --- /dev/null +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -0,0 +1,77 @@ +id(); + }); + + Schema::create('logins', function ($table) { + $table->id(); + $table->foreignId('user_id'); + }); + } + + protected function getEnvironmentSetUp($app) + { + parent::getEnvironmentSetUp($app); + $app['config']->set('app.debug', 'true'); + } + + public function testItOnlyEagerLoadsRequiredModels() + { + $this->retrievedLogins = 0; + User::getEventDispatcher()->listen('eloquent.retrieved:*', function ($event, $models) { + foreach ($models as $model) { + if (get_class($model) == Login::class) { + $this->retrievedLogins++; + } + } + }); + + $user = User::create(); + $user->latest_login()->create(); + $user->latest_login()->create(); + $user = User::create(); + $user->latest_login()->create(); + $user->latest_login()->create(); + + User::with('latest_login')->get(); + + $this->assertSame(2, $this->retrievedLogins); + } +} + +class User extends Model +{ + protected $guarded = []; + public $timestamps = false; + + public function latest_login() + { + return $this->hasOne(Login::class)->ofMany()->orderByDesc('id'); + } +} + +class Login extends Model +{ + protected $guarded = []; + public $timestamps = false; +} From b4a5de13cebb7669ebb65cc101b8d9e0c78832fa Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 13:21:53 +0200 Subject: [PATCH 02/39] Apply fixes from StyleCI --- .../Contracts/Database/Eloquent/PartialRelation.php | 4 ++-- .../Database/Eloquent/Concerns/QueriesRelationships.php | 6 +++--- .../Eloquent/Relations/Concerns/CanBeOneOfMany.php | 8 ++++---- .../Eloquent/Relations/Concerns/ComparesRelatedModels.php | 2 +- src/Illuminate/Database/Eloquent/Relations/HasOne.php | 4 ++-- tests/Database/DatabaseEloquentHasOneOfManyTest.php | 3 +-- tests/Integration/Database/EloquentHasOneOfManyTest.php | 7 ++----- 7 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php index baf94e6ac079..892609e4ef3c 100644 --- a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php +++ b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php @@ -7,7 +7,7 @@ interface PartialRelation /** * Wether the relation is a partial of a one-to-many relationship. * - * @param boolean $ofMany + * @param bool $ofMany * @return $this */ public function ofMany(bool $ofMany = true); @@ -15,7 +15,7 @@ public function ofMany(bool $ofMany = true); /** * Determines wether the relationship is one-of-many. * - * @return boolean + * @return bool */ public function isOneOfMany(); diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index c96bdc8c4e98..5e22be5836c7 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -412,7 +412,7 @@ public function withAggregate($relations, $column, $function = null) $query->orders = null; $query->setBindings([], 'order'); - if (count($query->columns) > 1 && !$this->isOneOfMany($relation)) { + if (count($query->columns) > 1 && ! $this->isOneOfMany($relation)) { $query->columns = [$query->columns[0]]; $query->bindings['select'] = []; } @@ -437,7 +437,7 @@ public function withAggregate($relations, $column, $function = null) * Determines whether the given relation is one-of-many. * * @param \Illuminate\Database\Eloquent\Relations\Relation $relation - * @return boolean + * @return bool */ protected function isOneOfMany($relation) { @@ -516,7 +516,7 @@ public function withAvg($relation, $column) */ protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean) { - if(!$this->isOneOfMany($relation)) { + if (! $this->isOneOfMany($relation)) { $hasQuery->mergeConstraintsFrom($relation->getQuery()); } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 4840ad55c2fb..fee5435bc481 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -27,13 +27,13 @@ trait CanBeOneOfMany * @var array */ protected $forwardToOneOfManyQuery = [ - 'get', 'exists', 'count' + 'get', 'exists', 'count', ]; /** * Wether the relation is a partial of a one-to-many relationship. * - * @param boolean $ofMany + * @param bool $ofMany * @return $this */ public function ofMany(bool $ofMany = true) @@ -84,7 +84,7 @@ protected function getSubSelectAlias() /** * Determines wether the relationship is one-of-many. * - * @return boolean + * @return bool */ public function isOneOfMany() { @@ -199,7 +199,7 @@ public function __call($method, $parameters) } $query = $this->query; - if($this->shouldForwardedToOneOfManyQuery($method)) { + if ($this->shouldForwardedToOneOfManyQuery($method)) { $query = $this->resolveOneOfManyQuery(); } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php index 6e1fa55cfffd..ee1bc586fed3 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -80,7 +80,7 @@ protected function compareOneOfMany($model) return true; } - if(! $this->isOneOfMany()) { + if (! $this->isOneOfMany()) { return true; } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index b8cf746b0c74..55a861a4eadd 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -89,7 +89,7 @@ protected function getRelatedKeyFrom(Model $model) */ public function addEagerConstraints(array $models) { - if(! $this->isOneOfMany()) { + if (! $this->isOneOfMany()) { return parent::addEagerConstraints($models); } @@ -112,7 +112,7 @@ public function addEagerConstraints(array $models) */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) { - if(! $this->isOneOfMany()) { + if (! $this->isOneOfMany()) { return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 8a87f8c933b7..bdcdaeb48192 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -2,9 +2,9 @@ namespace Illuminate\Tests\Database; -use PHPUnit\Framework\TestCase; use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Model as Eloquent; +use PHPUnit\Framework\TestCase; /** * @group one-of-many @@ -66,7 +66,6 @@ public function testItEagerLoadsCorrectModels() $this->assertSame($latestLogin->id, $user->latest_login->id); } - public function testHasNested() { $user = HasOneOfManyTestUser::create(); diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php index 1e7a2cb9ce77..2f84ac20d46d 100644 --- a/tests/Integration/Database/EloquentHasOneOfManyTest.php +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -2,11 +2,8 @@ namespace Illuminate\Tests\Integration\Database\EloquentHasOneOfManyTest; -use Illuminate\Support\Str; -use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Schema; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; use Illuminate\Tests\Integration\Database\DatabaseTestCase; /** @@ -45,7 +42,7 @@ public function testItOnlyEagerLoadsRequiredModels() } } }); - + $user = User::create(); $user->latest_login()->create(); $user->latest_login()->create(); From 1d60069e37dc1f8e5ba87798b3da6702d15dc063 Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 13:45:49 +0200 Subject: [PATCH 03/39] fixed getResults --- .../Database/Eloquent/Relations/HasOne.php | 8 ++++++- .../DatabaseEloquentHasOneOfManyTest.php | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index 55a861a4eadd..071b07e17064 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -25,7 +25,13 @@ public function getResults() return $this->getDefaultFor($this->parent); } - return $this->query->first() ?: $this->getDefaultFor($this->parent); + if($this->isOneOfMany()) { + $result = $this->resolveOneOfManyQuery()->first(); + } else { + $result = $this->query->first(); + } + + return $result ?: $this->getDefaultFor($this->parent); } /** diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index bdcdaeb48192..dfebb2787436 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -54,6 +54,27 @@ protected function tearDown(): void $this->schema()->drop('logins'); } + public function testItGetsCorrectResults() + { + $user = HasOneOfManyTestUser::create(); + $previousLogin = $user->logins()->create(); + $latestLogin = $user->logins()->create(); + + $result = $user->latest_login()->getResults(); + $this->assertNotNull($result); + $this->assertSame($latestLogin->id, $result->id); + } + + public function testItGetsWithConstraintsCorrectResults() + { + $user = HasOneOfManyTestUser::create(); + $previousLogin = $user->logins()->create(); + $user->logins()->create(); + + $result = $user->latest_login()->whereKey($previousLogin->getKey())->getResults(); + $this->assertNull($result); + } + public function testItEagerLoadsCorrectModels() { $user = HasOneOfManyTestUser::create(); From c13cd9a26b481026ba371c23716187c1b7fe5ae9 Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 13:48:27 +0200 Subject: [PATCH 04/39] added query methods to forwardToOneOfManyQuery --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index fee5435bc481..4b6f790d8583 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -27,7 +27,7 @@ trait CanBeOneOfMany * @var array */ protected $forwardToOneOfManyQuery = [ - 'get', 'exists', 'count', + 'get', 'exists', 'count', 'sum', 'avg', 'first', 'join', 'crossJoin' ]; /** From 61af2f44b692a63357ac4e611e00059d1cb39f7a Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 13:51:14 +0200 Subject: [PATCH 05/39] Apply fixes from StyleCI --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- src/Illuminate/Database/Eloquent/Relations/HasOne.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 4b6f790d8583..6670c960d31c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -27,7 +27,7 @@ trait CanBeOneOfMany * @var array */ protected $forwardToOneOfManyQuery = [ - 'get', 'exists', 'count', 'sum', 'avg', 'first', 'join', 'crossJoin' + 'get', 'exists', 'count', 'sum', 'avg', 'first', 'join', 'crossJoin', ]; /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index 071b07e17064..ccb4ef79ba77 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -25,7 +25,7 @@ public function getResults() return $this->getDefaultFor($this->parent); } - if($this->isOneOfMany()) { + if ($this->isOneOfMany()) { $result = $this->resolveOneOfManyQuery()->first(); } else { $result = $this->query->first(); From e76939c1e3ff679fd7988564e285540d1fe8f651 Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 18:15:57 +0200 Subject: [PATCH 06/39] improvements & tests --- .../Database/Eloquent/PartialRelation.php | 4 +- .../Relations/Concerns/CanBeOneOfMany.php | 59 ++++++++++++++----- .../DatabaseEloquentHasOneOfManyTest.php | 25 ++++++++ 3 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php index 892609e4ef3c..7bf7582d5462 100644 --- a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php +++ b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php @@ -7,10 +7,10 @@ interface PartialRelation /** * Wether the relation is a partial of a one-to-many relationship. * - * @param bool $ofMany + * @param string|null $relation * @return $this */ - public function ofMany(bool $ofMany = true); + public function ofMany($relation = null); /** * Determines wether the relationship is one-of-many. diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 6670c960d31c..b64a2ccb1031 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Relations\Relation; trait CanBeOneOfMany { @@ -20,6 +21,13 @@ trait CanBeOneOfMany */ protected $oneOfManyQuery; + /** + * The name of the relationship. + * + * @var string + */ + protected $relationName; + /** * The methods that should be forwarded to the one-of-many query builder * instance. @@ -33,20 +41,44 @@ trait CanBeOneOfMany /** * Wether the relation is a partial of a one-to-many relationship. * - * @param bool $ofMany + * @param string|null $ofMany * @return $this */ - public function ofMany(bool $ofMany = true) + public function ofMany($relation = null) { $this->isOneOfMany = true; - if ($ofMany) { - $this->setOneOfManyQuery(); + $this->setOneOfManyQuery(); + + if (! $this->relationName = $relation) { + $this->relationName = $this->guessRelationship(); } return $this; } + /** + * Guess the "hasOne" relationship name. + * + * @return string + */ + protected function guessRelationship() + { + [$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + + return $caller['function']; + } + + /** + * Get the name of the relationship. + * + * @return string + */ + public function getRelationName() + { + return $this->relationName; + } + /** * Initially set one-of-many parent query. * @@ -58,7 +90,7 @@ protected function setOneOfManyQuery() return; } - $this->oneOfManyQuery = $this->getOneOfManyQueryFor($this->query); + $this->oneOfManyQuery = $this->newOneOfManyQuery(); } /** @@ -76,9 +108,9 @@ protected function getRelatedKeyName() * * @return string */ - protected function getSubSelectAlias() + public function getSubSelectAlias() { - return $this->getRelatedTableName()."_{$this->localKey}_".spl_object_id($this); + return $this->getRelationName()."_{$this->localKey}"; } /** @@ -111,18 +143,18 @@ protected function addSubSelectConstraintsTo(Builder $query) * * @return string */ - protected function getSubSelectTableAlias() + public function getSubSelectTableAlias() { - return $this->getRelatedTableName().'_'.spl_object_id($this); + return $this->getRelationName(); } /** - * Get the qualified subselect column name. + * Get the qualified column name for the one-of-many subselect. * * @param string $column * @return string */ - protected function qualifySubSelectColumn($column) + public function qualifySubSelectColumn($column) { $segments = explode('.', $column); @@ -142,12 +174,11 @@ protected function getRelatedTableName() /** * Get the result query builder instance for the given query builder. * - * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - protected function getOneOfManyQueryFor(Builder $query) + protected function newOneOfManyQuery() { - return $query->clone(); + return $this->query->getModel()->newQuery(); } /** diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index dfebb2787436..501a935678e7 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -54,6 +54,24 @@ protected function tearDown(): void $this->schema()->drop('logins'); } + public function testItGuessesRelationName() + { + $user = HasOneOfManyTestUser::create(); + $this->assertSame('latest_login', $user->latest_login()->getRelationName()); + } + + public function testRelationNameCanBeSet() + { + $user = HasOneOfManyTestUser::create(); + $this->assertSame('foo', $user->latest_login_with_other_name()->getRelationName()); + } + + public function testQualifyingSubSelectColumn() + { + $user = HasOneOfManyTestUser::create(); + $this->assertSame('latest_login.id', $user->latest_login()->qualifySubSelectColumn('id')); + } + public function testItGetsCorrectResults() { $user = HasOneOfManyTestUser::create(); @@ -209,6 +227,13 @@ public function latest_login() { return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany()->orderByDesc('id'); } + + public function latest_login_with_other_name() + { + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id') + ->ofMany('foo') + ->orderByDesc('id'); + } } class HasOneOfManyTestLogin extends Eloquent From a899dc5dda3ec023b1d1fd6f78965c87fa83221e Mon Sep 17 00:00:00 2001 From: cbl Date: Tue, 4 May 2021 18:17:22 +0200 Subject: [PATCH 07/39] Apply fixes from StyleCI --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index b64a2ccb1031..9d538aacddd1 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -49,7 +49,7 @@ public function ofMany($relation = null) $this->isOneOfMany = true; $this->setOneOfManyQuery(); - + if (! $this->relationName = $relation) { $this->relationName = $this->guessRelationship(); } From 38e021af4769c07a4fe0b0356aa47e2af6c25f26 Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 5 May 2021 15:58:37 +0200 Subject: [PATCH 08/39] use where or having --- .../Relations/Concerns/CanBeOneOfMany.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 9d538aacddd1..71a539d34f98 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\SQLiteConnection; use Illuminate\Database\Eloquent\Relations\Relation; trait CanBeOneOfMany @@ -197,9 +198,18 @@ public function resolveOneOfManyQuery(Builder $query = null) return $this->oneOfManyQuery ->whereExists(function ($existsQuery) use ($query) { - $existsQuery - ->selectSub($query, $this->getSubSelectAlias()) - ->whereColumn($this->getSubSelectAlias(), $this->getRelatedKeyName()); + $existsQuery->selectSub($query, $this->getSubSelectAlias()); + + if ($query->getConnection() instanceof SQLiteConnection) { + $existsQuery->whereColumn( + $this->getSubSelectAlias(), + $this->getRelatedKeyName() + ); + } else { + $existsQuery->havingRaw( + $this->getSubSelectAlias() . ' = '. $this->getRelatedKeyName() + ); + } }); } From a8218c3ee2e314264ac7be0a3845a2ebb7411b14 Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 5 May 2021 15:59:36 +0200 Subject: [PATCH 09/39] Apply fixes from StyleCI --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 71a539d34f98..9204b2c40ca2 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -3,8 +3,8 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\SQLiteConnection; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\SQLiteConnection; trait CanBeOneOfMany { @@ -207,7 +207,7 @@ public function resolveOneOfManyQuery(Builder $query = null) ); } else { $existsQuery->havingRaw( - $this->getSubSelectAlias() . ' = '. $this->getRelatedKeyName() + $this->getSubSelectAlias().' = '.$this->getRelatedKeyName() ); } }); From 19354b88daa13b1675334c71d53c3a03d78a3c8f Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 12 May 2021 16:55:35 +0200 Subject: [PATCH 10/39] join --- .../Database/Eloquent/PartialRelation.php | 12 +- .../Concerns/QueriesRelationships.php | 35 ++--- .../Relations/Concerns/CanBeOneOfMany.php | 137 ++---------------- .../Concerns/ComparesRelatedModels.php | 2 +- .../Database/Eloquent/Relations/HasOne.php | 52 +------ .../DatabaseEloquentHasOneOfManyTest.php | 24 +-- .../Database/EloquentHasOneOfManyTest.php | 4 +- 7 files changed, 53 insertions(+), 213 deletions(-) diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php index 7bf7582d5462..e3a748312599 100644 --- a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php +++ b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php @@ -2,6 +2,8 @@ namespace Illuminate\Contracts\Database\Eloquent; +use Closure; + interface PartialRelation { /** @@ -10,7 +12,7 @@ interface PartialRelation * @param string|null $relation * @return $this */ - public function ofMany($relation = null); + public function ofMany(Closure $closure = null); /** * Determines wether the relationship is one-of-many. @@ -18,12 +20,4 @@ public function ofMany($relation = null); * @return bool */ public function isOneOfMany(); - - /** - * Resolve the one-of-many query. - * - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return \Illuminate\Database\Eloquent\Builder - */ - public function resolveOneOfManyQuery(); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 5e22be5836c7..8327b1c14bdf 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -3,7 +3,6 @@ namespace Illuminate\Database\Eloquent\Concerns; use Closure; -use Illuminate\Contracts\Database\Eloquent\PartialRelation; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\Relation; @@ -47,7 +46,8 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', C : 'getRelationExistenceCountQuery'; $hasQuery = $relation->{$method}( - $relation->getRelated()->newQueryWithoutRelationships(), $this + $relation->getRelated()->newQueryWithoutRelationships(), + $this ); // Next we will call any given callback as an "anonymous" scope so they can get the @@ -58,7 +58,11 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', C } return $this->addHasWhere( - $hasQuery, $relation, $operator, $count, $boolean + $hasQuery, + $relation, + $operator, + $count, + $boolean ); } @@ -399,7 +403,9 @@ public function withAggregate($relations, $column, $function = null) // as a sub-select. First, we'll get the "has" query and use that to get the relation // sub-query. We'll format this relationship name and append this column if needed. $query = $relation->getRelationExistenceQuery( - $relation->getRelated()->newQuery(), $this, new Expression($expression) + $relation->getRelated()->newQuery(), + $this, + new Expression($expression) )->setBindings([], 'select'); $query->callScope($constraints); @@ -412,7 +418,7 @@ public function withAggregate($relations, $column, $function = null) $query->orders = null; $query->setBindings([], 'order'); - if (count($query->columns) > 1 && ! $this->isOneOfMany($relation)) { + if (count($query->columns) > 1) { $query->columns = [$query->columns[0]]; $query->bindings['select'] = []; } @@ -433,18 +439,6 @@ public function withAggregate($relations, $column, $function = null) return $this; } - /** - * Determines whether the given relation is one-of-many. - * - * @param \Illuminate\Database\Eloquent\Relations\Relation $relation - * @return bool - */ - protected function isOneOfMany($relation) - { - return $relation instanceof PartialRelation - && $relation->isOneOfMany(); - } - /** * Add subselect queries to count the relations. * @@ -516,9 +510,7 @@ public function withAvg($relation, $column) */ protected function addHasWhere(Builder $hasQuery, Relation $relation, $operator, $count, $boolean) { - if (! $this->isOneOfMany($relation)) { - $hasQuery->mergeConstraintsFrom($relation->getQuery()); - } + $hasQuery->mergeConstraintsFrom($relation->getQuery()); return $this->canUseExistsForExistenceCheck($operator, $count) ? $this->addWhereExistsQuery($hasQuery->toBase(), $boolean, $operator === '<' && $count === 1) @@ -541,7 +533,8 @@ public function mergeConstraintsFrom(Builder $from) return $this->withoutGlobalScopes( $from->removedScopes() )->mergeWheres( - $from->getQuery()->wheres, $whereBindings + $from->getQuery()->wheres, + $whereBindings ); } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 9204b2c40ca2..12e43d417c5d 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -2,9 +2,11 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; +use Closure; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Query\JoinClause; use Illuminate\Database\SQLiteConnection; +use Illuminate\Database\Eloquent\Relations\Relation; trait CanBeOneOfMany { @@ -15,13 +17,6 @@ trait CanBeOneOfMany */ protected $isOneOfMany = false; - /** - * The one-of-many parent query builder. - * - * @var \Illuminate\Database\Eloquent\Builder - */ - protected $oneOfManyQuery; - /** * The name of the relationship. * @@ -29,32 +24,30 @@ trait CanBeOneOfMany */ protected $relationName; - /** - * The methods that should be forwarded to the one-of-many query builder - * instance. - * - * @var array - */ - protected $forwardToOneOfManyQuery = [ - 'get', 'exists', 'count', 'sum', 'avg', 'first', 'join', 'crossJoin', - ]; - /** * Wether the relation is a partial of a one-to-many relationship. * * @param string|null $ofMany * @return $this */ - public function ofMany($relation = null) + public function ofMany(Closure $closure = null) { $this->isOneOfMany = true; - $this->setOneOfManyQuery(); + $this->relationName = $this->guessRelationship(); + + $sub = $this->query->getModel()->newQuery() + ->groupBy($this->foreignKey); - if (! $this->relationName = $relation) { - $this->relationName = $this->guessRelationship(); + if ($closure instanceof Closure) { + $closure($sub); } + $this->query->joinSub($sub, $this->relationName, function ($join) { + $key = $this->query->getModel()->getKeyName(); + $join->on($this->qualifySubSelectColumn($key), '=', $this->query->getModel()->getTable() . '.'.$key); + }); + return $this; } @@ -80,20 +73,6 @@ public function getRelationName() return $this->relationName; } - /** - * Initially set one-of-many parent query. - * - * @return void - */ - protected function setOneOfManyQuery() - { - if ($this->oneOfManyQuery) { - return; - } - - $this->oneOfManyQuery = $this->newOneOfManyQuery(); - } - /** * Get related key name. * @@ -124,21 +103,6 @@ public function isOneOfMany() return $this->isOneOfMany; } - /** - * Add subselect contstraints to the given query builder. - * - * @param Builder $query - * @return void - */ - protected function addSubSelectConstraintsTo(Builder $query) - { - $query - ->from($this->getRelatedTableName(), $this->getSubSelectTableAlias()) - ->whereColumn($this->qualifySubSelectColumn($this->foreignKey), $this->foreignKey) - ->select($this->qualifySubSelectColumn($query->getModel()->getKeyName())) - ->take(1); - } - /** * Get the subselect table alias. * @@ -181,75 +145,4 @@ protected function newOneOfManyQuery() { return $this->query->getModel()->newQuery(); } - - /** - * Resolve the one-of-many query. - * - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return \Illuminate\Database\Eloquent\Builder - */ - public function resolveOneOfManyQuery(Builder $query = null) - { - if (is_null($query)) { - $query = $this->query; - } - - $this->addSubSelectConstraintsTo($query); - - return $this->oneOfManyQuery - ->whereExists(function ($existsQuery) use ($query) { - $existsQuery->selectSub($query, $this->getSubSelectAlias()); - - if ($query->getConnection() instanceof SQLiteConnection) { - $existsQuery->whereColumn( - $this->getSubSelectAlias(), - $this->getRelatedKeyName() - ); - } else { - $existsQuery->havingRaw( - $this->getSubSelectAlias().' = '.$this->getRelatedKeyName() - ); - } - }); - } - - /** - * Determines wether the given query method should be forwarded to the - * one-of-many query. - * - * @param string $method - * @return bool - */ - protected function shouldForwardedToOneOfManyQuery($method) - { - return $this->isOneOfMany() - && in_array($method, $this->forwardToOneOfManyQuery); - } - - /** - * Handle dynamic method calls to the relationship. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) - { - if (static::hasMacro($method)) { - return $this->macroCall($method, $parameters); - } - - $query = $this->query; - if ($this->shouldForwardedToOneOfManyQuery($method)) { - $query = $this->resolveOneOfManyQuery(); - } - - $result = $this->forwardCallTo($query, $method, $parameters); - - if ($result === $query) { - return $this; - } - - return $result; - } } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php index ee1bc586fed3..c5a63d9f6bcd 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -84,7 +84,7 @@ protected function compareOneOfMany($model) return true; } - return $this->resolveOneOfManyQuery() + return $this->query ->whereKey($model->getKey()) ->exists(); } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index ccb4ef79ba77..3dfa8bd275f7 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -25,13 +25,7 @@ public function getResults() return $this->getDefaultFor($this->parent); } - if ($this->isOneOfMany()) { - $result = $this->resolveOneOfManyQuery()->first(); - } else { - $result = $this->query->first(); - } - - return $result ?: $this->getDefaultFor($this->parent); + return $this->query->first() ?: $this->getDefaultFor($this->parent); } /** @@ -87,25 +81,6 @@ protected function getRelatedKeyFrom(Model $model) return $model->getAttribute($this->getForeignKeyName()); } - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - * @return void - */ - public function addEagerConstraints(array $models) - { - if (! $this->isOneOfMany()) { - return parent::addEagerConstraints($models); - } - - $whereIn = $this->whereInMethod($this->parent, $this->localKey); - - $this->oneOfManyQuery->{$whereIn}( - $this->foreignKey, $this->getKeys($models, $this->localKey) - ); - } - /** * Add the constraints for an internal relationship existence query. * @@ -122,29 +97,10 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } - $query->whereColumn( + $query->getQuery()->joins = $this->query->getQuery()->joins; + + return $query->select($columns)->whereColumn( $this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey() ); - - $query->getQuery()->orders = $this->query->getQuery()->orders; - - $this->oneOfManyQuery->select($columns); - - return $this->resolveOneOfManyQuery($query); - } - - /** - * Execute the query as a "select" statement. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Collection - */ - public function get($columns = ['*']) - { - if (! $this->isOneOfMany()) { - return parent::get($columns); - } - - return $this->resolveOneOfManyQuery()->get($columns); } } diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 501a935678e7..025120cea320 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -60,11 +60,11 @@ public function testItGuessesRelationName() $this->assertSame('latest_login', $user->latest_login()->getRelationName()); } - public function testRelationNameCanBeSet() - { - $user = HasOneOfManyTestUser::create(); - $this->assertSame('foo', $user->latest_login_with_other_name()->getRelationName()); - } + // public function testRelationNameCanBeSet() + // { + // $user = HasOneOfManyTestUser::create(); + // $this->assertSame('foo', $user->latest_login_with_other_name()->getRelationName()); + // } public function testQualifyingSubSelectColumn() { @@ -112,12 +112,12 @@ public function testHasNested() $latestLogin = $user->logins()->create(); $found = HasOneOfManyTestUser::whereHas('latest_login', function ($query) use ($latestLogin) { - $query->where('id', $latestLogin->id); + $query->where('logins.id', $latestLogin->id); })->exists(); $this->assertTrue($found); $found = HasOneOfManyTestUser::whereHas('latest_login', function ($query) use ($previousLogin) { - $query->where('id', $previousLogin->id); + $query->where('logins.id', $previousLogin->id); })->exists(); $this->assertFalse($found); } @@ -225,14 +225,16 @@ public function logins() public function latest_login() { - return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany()->orderByDesc('id'); + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(function($q) { + $q->selectRaw('MAX(id) as id'); + }); } public function latest_login_with_other_name() { - return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id') - ->ofMany('foo') - ->orderByDesc('id'); + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(function($q) { + $q->selectRaw('MAX(id) as id'); + }); } } diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php index 2f84ac20d46d..b53efc1557f1 100644 --- a/tests/Integration/Database/EloquentHasOneOfManyTest.php +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -63,7 +63,9 @@ class User extends Model public function latest_login() { - return $this->hasOne(Login::class)->ofMany()->orderByDesc('id'); + return $this->hasOne(Login::class)->ofMany(function($q) { + $q->selectRaw('MAX(id) as id'); + }); } } From 3fd1afff5b1635fa304dded4b73c2662d1083093 Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 12 May 2021 18:03:32 +0200 Subject: [PATCH 11/39] wip --- .../Relations/Concerns/CanBeOneOfMany.php | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 12e43d417c5d..04269cffb07c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -27,25 +27,33 @@ trait CanBeOneOfMany /** * Wether the relation is a partial of a one-to-many relationship. * - * @param string|null $ofMany + * @param Closure|string|null $column + * @param string|null $relation * @return $this */ - public function ofMany(Closure $closure = null) + public function ofMany($column = null, $aggregate = 'MAX', $relation = null) { $this->isOneOfMany = true; - $this->relationName = $this->guessRelationship(); + if (is_null($this->relationName = $relation)) { + $this->relationName = $this->guessRelationship(); + } $sub = $this->query->getModel()->newQuery() ->groupBy($this->foreignKey); - if ($closure instanceof Closure) { - $closure($sub); + $keyName = $this->query->getModel()->getKeyName(); + + if ($column instanceof Closure) { + $column($sub); + } else { + $sub->selectRaw( + $aggregate.'('.$column.')' . $column == $keyName ? " as {$column}" : ", {$keyName}" + ); } - $this->query->joinSub($sub, $this->relationName, function ($join) { - $key = $this->query->getModel()->getKeyName(); - $join->on($this->qualifySubSelectColumn($key), '=', $this->query->getModel()->getTable() . '.'.$key); + $this->query->joinSub($sub, $this->relationName, function ($join) use ($keyName) { + $join->on($this->qualifySubSelectColumn($keyName), '=', $this->query->getModel()->getTable() . '.'.$keyName); }); return $this; From 176432e13f3e0315fcdb3cb1237ce2115f75a83f Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 12 May 2021 18:07:33 +0200 Subject: [PATCH 12/39] wip --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 04269cffb07c..106a490163c5 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -31,7 +31,7 @@ trait CanBeOneOfMany * @param string|null $relation * @return $this */ - public function ofMany($column = null, $aggregate = 'MAX', $relation = null) + public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) { $this->isOneOfMany = true; From 5e0408ce626d2f70cc99e78d06089cfd8d71e8aa Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 12 May 2021 21:43:31 +0200 Subject: [PATCH 13/39] fixes style --- .../Eloquent/Concerns/QueriesRelationships.php | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 8327b1c14bdf..7456fc6e4a6f 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -46,8 +46,7 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', C : 'getRelationExistenceCountQuery'; $hasQuery = $relation->{$method}( - $relation->getRelated()->newQueryWithoutRelationships(), - $this + $relation->getRelated()->newQueryWithoutRelationships(), $this ); // Next we will call any given callback as an "anonymous" scope so they can get the @@ -58,11 +57,7 @@ public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', C } return $this->addHasWhere( - $hasQuery, - $relation, - $operator, - $count, - $boolean + $hasQuery, $relation, $operator, $count, $boolean ); } @@ -403,9 +398,7 @@ public function withAggregate($relations, $column, $function = null) // as a sub-select. First, we'll get the "has" query and use that to get the relation // sub-query. We'll format this relationship name and append this column if needed. $query = $relation->getRelationExistenceQuery( - $relation->getRelated()->newQuery(), - $this, - new Expression($expression) + $relation->getRelated()->newQuery(), $this, new Expression($expression) )->setBindings([], 'select'); $query->callScope($constraints); @@ -533,8 +526,7 @@ public function mergeConstraintsFrom(Builder $from) return $this->withoutGlobalScopes( $from->removedScopes() )->mergeWheres( - $from->getQuery()->wheres, - $whereBindings + $from->getQuery()->wheres, $whereBindings ); } From 33ce8a83da6faa2505cad6a3105a706e8b131afa Mon Sep 17 00:00:00 2001 From: cbl Date: Wed, 12 May 2021 21:45:08 +0200 Subject: [PATCH 14/39] updated contract --- src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php index e3a748312599..9de389a55068 100644 --- a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php +++ b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php @@ -9,10 +9,11 @@ interface PartialRelation /** * Wether the relation is a partial of a one-to-many relationship. * + * @param Closure|string|null $column * @param string|null $relation * @return $this */ - public function ofMany(Closure $closure = null); + public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null); /** * Determines wether the relationship is one-of-many. From 626fb00463699d649a5d3873a8d9901239a6cb43 Mon Sep 17 00:00:00 2001 From: cbl Date: Thu, 13 May 2021 19:28:54 +0200 Subject: [PATCH 15/39] multiple aggregastes --- .../Relations/Concerns/CanBeOneOfMany.php | 98 ++++++++++++--- .../DatabaseEloquentHasOneOfManyTest.php | 117 ++++++++++++++++-- .../Database/EloquentHasOneOfManyTest.php | 4 +- 3 files changed, 192 insertions(+), 27 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 106a490163c5..4522c7d4212c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -3,10 +3,8 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; use Closure; +use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Query\JoinClause; -use Illuminate\Database\SQLiteConnection; -use Illuminate\Database\Eloquent\Relations\Relation; trait CanBeOneOfMany { @@ -27,7 +25,8 @@ trait CanBeOneOfMany /** * Wether the relation is a partial of a one-to-many relationship. * - * @param Closure|string|null $column + * @param string|array|null $column + * @param string|Closure|null $aggregate * @param string|null $relation * @return $this */ @@ -39,26 +38,82 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) $this->relationName = $this->guessRelationship(); } - $sub = $this->query->getModel()->newQuery() - ->groupBy($this->foreignKey); - $keyName = $this->query->getModel()->getKeyName(); + + if (is_string($columns = $column)) { + $columns = [ + $column => $aggregate, + $keyName => $aggregate + ]; + } - if ($column instanceof Closure) { - $column($sub); - } else { - $sub->selectRaw( - $aggregate.'('.$column.')' . $column == $keyName ? " as {$column}" : ", {$keyName}" - ); + if ($aggregate instanceof Closure) { + $closure = $aggregate; + } + + foreach ($columns as $column => $aggregate) { + $groupBy = isset($previous) ? $previous['column'] : $this->foreignKey; + + $sub = $this->newSubQuery($groupBy, $column, $aggregate); + + if (isset($previous)) { + $this->addJoinSub($sub, $previous['sub'], $previous['column']); + } elseif (isset($closure)) { + $closure($sub); + } + + if (array_key_last($columns) == $column) { + $this->addJoinSub($this->query, $sub, $column); + } + + $previous = [ + 'sub' => $sub, + 'column' => $column + ]; } - $this->query->joinSub($sub, $this->relationName, function ($join) use ($keyName) { - $join->on($this->qualifySubSelectColumn($keyName), '=', $this->query->getModel()->getTable() . '.'.$keyName); - }); return $this; } + /** + * Get new grouped sub query for inner join clause. + * + * @param string $groupBy + * @param string|null $column + * @param string|null $aggregate + * @return void + */ + protected function newSubQuery($groupBy, $column = null, $aggregate = null) + { + $sub = $this->query->getModel() + ->newQuery() + ->groupBy($this->qualifyRelatedColumn($groupBy)); + + if (!is_null($column)) { + $sub->selectRaw($aggregate.'('.$column.') as '.$column.','.$this->foreignKey); + } + + return $sub; + } + + /** + * Add join sub. + * + * @param Builder $parent + * @param Builder $sub + * @param string $on + * @return void + */ + protected function addJoinSub(Builder $parent, Builder $sub, $on) + { + $parent->joinSub($sub, $this->relationName, function ($join) use ($on) { + $join + ->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)) + ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + }); + } + /** * Guess the "hasOne" relationship name. * @@ -121,6 +176,17 @@ public function getSubSelectTableAlias() return $this->getRelationName(); } + /** + * Qualify related column. + * + * @param string $column + * @return string + */ + protected function qualifyRelatedColumn($column) + { + return Str::contains($column, '.') ? $column : $this->getRelatedTableName().".".$column; + } + /** * Get the qualified column name for the one-of-many subselect. * diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 025120cea320..715c0e1806dd 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -16,8 +16,8 @@ protected function setUp(): void $db = new DB; $db->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', + 'driver' => 'sqlite', + 'database' => ':memory:', ]); $db->bootEloquent(); @@ -41,6 +41,19 @@ public function createSchema() $table->increments('id'); $table->foreignId('user_id'); }); + + $this->schema()->create('states', function ($table) { + $table->increments('id'); + $table->string('state'); + $table->string('type'); + $table->foreignId('user_id'); + }); + + $this->schema()->create('prices', function ($table) { + $table->increments('id'); + $table->dateTime('published_at'); + $table->foreignId('user_id'); + }); } /** @@ -188,6 +201,51 @@ public function testCount() $this->assertSame(1, $user->latest_login()->count()); } + public function testAggregate() + { + $user = HasOneOfManyTestUser::create(); + $firstLogin = $user->logins()->create(); + $user->logins()->create(); + + $user = HasOneOfManyTestUser::first(); + $this->assertSame($firstLogin->id, $user->first_login->id); + } + + public function testJoinConstraints() + { + $user = HasOneOfManyTestUser::create(); + $user->states()->create([ + 'type' => 'foo', + 'state' => 'draft', + ]); + $currentForState = $user->states()->create([ + 'type' => 'foo', + 'state' => 'active', + ]); + $user->states()->create([ + 'type' => 'bar', + 'state' => 'baz', + ]); + + $user = HasOneOfManyTestUser::first(); + $this->assertSame($currentForState->id, $user->foo_state->id); + } + + public function testMultipleAggregates() + { + $user = HasOneOfManyTestUser::create(); + + $user->prices()->create([ + 'published_at' => '2021-05-01 00:00:00', + ]); + $price = $user->prices()->create([ + 'published_at' => '2021-05-01 00:00:00', + ]); + + $user = HasOneOfManyTestUser::first(); + $this->assertSame($price->id, $user->price->id); + } + /** * Get a database connection instance. * @@ -225,15 +283,41 @@ public function logins() public function latest_login() { - return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(function($q) { - $q->selectRaw('MAX(id) as id'); - }); + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(); + } + + public function first_login() + { + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany('id', 'min'); + } + + public function states() + { + return $this->hasMany(HasOneOfManyTestState::class, 'user_id'); + } + + public function foo_state() + { + return $this->hasOne(HasOneOfManyTestState::class, 'user_id')->ofMany( + ['id' => 'max'], + function ($q) { + $q->where('type', 'foo'); + } + ); } - public function latest_login_with_other_name() + public function prices() { - return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(function($q) { - $q->selectRaw('MAX(id) as id'); + return $this->hasMany(HasOneOfManyTestPrice::class, 'user_id'); + } + + public function price() + { + return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->ofMany([ + 'published_at' => 'max', + 'id' => 'max', + ], function ($q) { + $q->where('published_at', '<', now()); }); } } @@ -244,3 +328,20 @@ class HasOneOfManyTestLogin extends Eloquent protected $guarded = []; public $timestamps = false; } + +class HasOneOfManyTestState extends Eloquent +{ + protected $table = 'states'; + protected $guarded = []; + public $timestamps = false; + protected $fillable = ['type', 'state']; +} + +class HasOneOfManyTestPrice extends Eloquent +{ + protected $table = 'prices'; + protected $guarded = []; + public $timestamps = false; + protected $fillable = ['published_at']; + protected $casts = ['published_at' => 'datetime']; +} diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php index b53efc1557f1..96c81eb27f76 100644 --- a/tests/Integration/Database/EloquentHasOneOfManyTest.php +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -63,9 +63,7 @@ class User extends Model public function latest_login() { - return $this->hasOne(Login::class)->ofMany(function($q) { - $q->selectRaw('MAX(id) as id'); - }); + return $this->hasOne(Login::class)->ofMany(); } } From 571db720abbb41c3631577037039c89d85b54be0 Mon Sep 17 00:00:00 2001 From: cbl Date: Thu, 13 May 2021 19:30:35 +0200 Subject: [PATCH 16/39] Apply fixes from StyleCI --- .../Relations/Concerns/CanBeOneOfMany.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 4522c7d4212c..d7a8d7f31276 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -3,8 +3,8 @@ namespace Illuminate\Database\Eloquent\Relations\Concerns; use Closure; -use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Str; trait CanBeOneOfMany { @@ -39,11 +39,11 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) } $keyName = $this->query->getModel()->getKeyName(); - + if (is_string($columns = $column)) { $columns = [ $column => $aggregate, - $keyName => $aggregate + $keyName => $aggregate, ]; } @@ -68,11 +68,10 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) $previous = [ 'sub' => $sub, - 'column' => $column + 'column' => $column, ]; } - return $this; } @@ -90,10 +89,10 @@ protected function newSubQuery($groupBy, $column = null, $aggregate = null) ->newQuery() ->groupBy($this->qualifyRelatedColumn($groupBy)); - if (!is_null($column)) { + if (! is_null($column)) { $sub->selectRaw($aggregate.'('.$column.') as '.$column.','.$this->foreignKey); } - + return $sub; } @@ -184,7 +183,7 @@ public function getSubSelectTableAlias() */ protected function qualifyRelatedColumn($column) { - return Str::contains($column, '.') ? $column : $this->getRelatedTableName().".".$column; + return Str::contains($column, '.') ? $column : $this->getRelatedTableName().'.'.$column; } /** From 6acf8264ca44592886dfb916fc6ed201880ca135 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 14 May 2021 15:30:22 -0500 Subject: [PATCH 17/39] formatting --- .../Database/Eloquent/PartialRelation.php | 5 +- .../Relations/Concerns/CanBeOneOfMany.php | 156 ++++++------------ .../Database/Eloquent/Relations/HasOne.php | 8 +- 3 files changed, 56 insertions(+), 113 deletions(-) diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php index 9de389a55068..8e2191b3c318 100644 --- a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php +++ b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php @@ -7,16 +7,17 @@ interface PartialRelation { /** - * Wether the relation is a partial of a one-to-many relationship. + * Indicate that the relation is a partial of a one-to-many relationship. * * @param Closure|string|null $column * @param string|null $relation + * @param string $relation * @return $this */ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null); /** - * Determines wether the relationship is one-of-many. + * Determine whether the relationship is a one-of-many relationship. * * @return bool */ diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index d7a8d7f31276..bc393da7c32d 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -9,7 +9,7 @@ trait CanBeOneOfMany { /** - * Determines wether the relationship is one-of-many. + * Determines whether the relationship is one-of-many. * * @var bool */ @@ -23,52 +23,49 @@ trait CanBeOneOfMany protected $relationName; /** - * Wether the relation is a partial of a one-to-many relationship. + * whether the relation is a partial of a one-to-many relationship. * - * @param string|array|null $column - * @param string|Closure|null $aggregate - * @param string|null $relation + * @param string|array|null $column + * @param string|Closure|null $aggregate + * @param string|null $relation * @return $this */ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) { $this->isOneOfMany = true; - if (is_null($this->relationName = $relation)) { - $this->relationName = $this->guessRelationship(); - } + $this->relationName = $relation ?: $this->guessRelationship(); $keyName = $this->query->getModel()->getKeyName(); - if (is_string($columns = $column)) { - $columns = [ - $column => $aggregate, - $keyName => $aggregate, - ]; - } + $columns = is_string($columns = $column) ? [ + $column => $aggregate, + $keyName => $aggregate, + ] : $column; if ($aggregate instanceof Closure) { $closure = $aggregate; } foreach ($columns as $column => $aggregate) { - $groupBy = isset($previous) ? $previous['column'] : $this->foreignKey; - - $sub = $this->newSubQuery($groupBy, $column, $aggregate); + $subQuery = $this->newSubQuery( + isset($previous) ? $previous['column'] : $this->foreignKey, + $column, $aggregate + ); if (isset($previous)) { - $this->addJoinSub($sub, $previous['sub'], $previous['column']); + $this->addJoinSub($subQuery, $previous['sub'], $previous['column']); } elseif (isset($closure)) { - $closure($sub); + $closure($subQuery); } if (array_key_last($columns) == $column) { - $this->addJoinSub($this->query, $sub, $column); + $this->addJoinSub($this->query, $subQuery, $column); } $previous = [ - 'sub' => $sub, - 'column' => $column, + 'sub' => $subQuery, + 'column' => $column, ]; } @@ -76,87 +73,76 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) } /** - * Get new grouped sub query for inner join clause. + * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship. * - * @param string $groupBy - * @param string|null $column - * @param string|null $aggregate + * @param string $groupBy + * @param string|null $column + * @param string|null $aggregate * @return void */ protected function newSubQuery($groupBy, $column = null, $aggregate = null) { - $sub = $this->query->getModel() + $subQuery = $this->query->getModel() ->newQuery() ->groupBy($this->qualifyRelatedColumn($groupBy)); if (! is_null($column)) { - $sub->selectRaw($aggregate.'('.$column.') as '.$column.','.$this->foreignKey); + $subQuery->selectRaw($aggregate.'('.$column.') as '.$column.', '.$this->foreignKey); } - return $sub; + return $subQuery; } /** - * Add join sub. + * Add the join subquery to the given query on the given column and the relationship's foreign key. * - * @param Builder $parent - * @param Builder $sub - * @param string $on + * @param \Illuminate\Database\Eloquent\Builder $parent + * @param \Illuminate\Database\Eloquent\Builder $subQuery + * @param string $on * @return void */ - protected function addJoinSub(Builder $parent, Builder $sub, $on) + protected function addJoinSub(Builder $parent, Builder $subQuery, $on) { - $parent->joinSub($sub, $this->relationName, function ($join) use ($on) { - $join - ->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)) - ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { + $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)) + ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); }); } /** - * Guess the "hasOne" relationship name. - * - * @return string - */ - protected function guessRelationship() - { - [$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - - return $caller['function']; - } - - /** - * Get the name of the relationship. + * Get the qualified column name for the one-of-many relationship using the subselect join query's alias. * + * @param string $column * @return string */ - public function getRelationName() + public function qualifySubSelectColumn($column) { - return $this->relationName; + return $this->getRelationName().'.'.last(explode('.', $column)); } /** - * Get related key name. + * Qualify related column using the related table name if it is not already qualified. * + * @param string $column * @return string */ - protected function getRelatedKeyName() + protected function qualifyRelatedColumn($column) { - return $this->getRelatedTableName().'.'.$this->query->getModel()->getKeyName(); + return Str::contains($column, '.') ? $column : $this->query->getModel()->getTable().'.'.$column; } /** - * Get sub select alias. + * Guess the "hasOne" relationship's name via backtrace. * * @return string */ - public function getSubSelectAlias() + protected function guessRelationship() { - return $this->getRelationName()."_{$this->localKey}"; + return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['function']; } /** - * Determines wether the relationship is one-of-many. + * Determine whether the relationship is a one-of-many relationship. * * @return bool */ @@ -166,56 +152,12 @@ public function isOneOfMany() } /** - * Get the subselect table alias. - * - * @return string - */ - public function getSubSelectTableAlias() - { - return $this->getRelationName(); - } - - /** - * Qualify related column. - * - * @param string $column - * @return string - */ - protected function qualifyRelatedColumn($column) - { - return Str::contains($column, '.') ? $column : $this->getRelatedTableName().'.'.$column; - } - - /** - * Get the qualified column name for the one-of-many subselect. - * - * @param string $column - * @return string - */ - public function qualifySubSelectColumn($column) - { - $segments = explode('.', $column); - - return $this->getSubSelectTableAlias().'.'.end($segments); - } - - /** - * Get the related table name. + * Get the name of the relationship. * * @return string */ - protected function getRelatedTableName() - { - return $this->query->getModel()->getTable(); - } - - /** - * Get the result query builder instance for the given query builder. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function newOneOfManyQuery() + public function getRelationName() { - return $this->query->getModel()->newQuery(); + return $this->relationName; } } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index 3dfa8bd275f7..17df79dc28d8 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -84,11 +84,11 @@ protected function getRelatedKeyFrom(Model $model) /** * Add the constraints for an internal relationship existence query. * - * Essentially, these queries compare on column names like whereColumn. + * Essentially, these queries compare on column names like "whereColumn". * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns * @return \Illuminate\Database\Eloquent\Builder */ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) From bfe124b6b154eb0e21c63b8310bb49c8689106b3 Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Fri, 14 May 2021 23:15:38 +0200 Subject: [PATCH 18/39] formatting --- .../Concerns/ComparesRelatedModels.php | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php index c5a63d9f6bcd..2994748ab718 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -15,13 +15,27 @@ trait ComparesRelatedModels */ public function is($model) { - return ! is_null($model) && + $match = ! is_null($model) && $this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) && $this->related->getTable() === $model->getTable() && - $this->related->getConnectionName() === $model->getConnectionName() && - $this->compareOneOfMany($model); + $this->related->getConnectionName() === $model->getConnectionName(); + + if(! $match) { + return false; + } + + if(! $this instanceof PartialRelation && ! $this->isOneOfMany()) { + return $match; + } + + // For "one-of-many" relationships, existence must be checked since keys + // also match for models that are not the related instance of the relationship. + return $this->query + ->whereKey($model->getKey()) + ->exists(); } + /** * Determine if the model is not the related instance of the relationship. * @@ -67,25 +81,4 @@ protected function compareKeys($parentKey, $relatedKey) return $parentKey === $relatedKey; } - - /** - * Determine if the given model is the correct relationship model. - * - * @param \Illuminate\Database\Eloquent\Model|null $model - * @return bool - */ - protected function compareOneOfMany($model) - { - if (! $this instanceof PartialRelation) { - return true; - } - - if (! $this->isOneOfMany()) { - return true; - } - - return $this->query - ->whereKey($model->getKey()) - ->exists(); - } } From 9b8e1f6d5edbe0cde674c39b29e1f67b5ade16af Mon Sep 17 00:00:00 2001 From: cbl Date: Fri, 14 May 2021 23:20:24 +0200 Subject: [PATCH 19/39] Apply fixes from StyleCI --- .../Relations/Concerns/ComparesRelatedModels.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php index 2994748ab718..54f981fdd13f 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -19,23 +19,22 @@ public function is($model) $this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) && $this->related->getTable() === $model->getTable() && $this->related->getConnectionName() === $model->getConnectionName(); - - if(! $match) { + + if (! $match) { return false; } - - if(! $this instanceof PartialRelation && ! $this->isOneOfMany()) { + + if (! $this instanceof PartialRelation && ! $this->isOneOfMany()) { return $match; } - - // For "one-of-many" relationships, existence must be checked since keys + + // For "one-of-many" relationships, existence must be checked since keys // also match for models that are not the related instance of the relationship. return $this->query ->whereKey($model->getKey()) ->exists(); } - /** * Determine if the model is not the related instance of the relationship. * From 23769919da0ff970b24fceafefa7713730ba8d8a Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 14 May 2021 16:24:11 -0500 Subject: [PATCH 20/39] formatting --- .../Concerns/ComparesRelatedModels.php | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php index 2994748ab718..fc8722046dbf 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/ComparesRelatedModels.php @@ -19,20 +19,14 @@ public function is($model) $this->compareKeys($this->getParentKey(), $this->getRelatedKeyFrom($model)) && $this->related->getTable() === $model->getTable() && $this->related->getConnectionName() === $model->getConnectionName(); - - if(! $match) { - return false; - } - - if(! $this instanceof PartialRelation && ! $this->isOneOfMany()) { - return $match; + + if ($match && $this instanceof PartialRelation && $this->isOneOfMany()) { + return $this->query + ->whereKey($model->getKey()) + ->exists(); } - - // For "one-of-many" relationships, existence must be checked since keys - // also match for models that are not the related instance of the relationship. - return $this->query - ->whereKey($model->getKey()) - ->exists(); + + return $match; } From dff1bf33948008867d8a503bf51d64c49efa0e7f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 14 May 2021 16:29:15 -0500 Subject: [PATCH 21/39] rename class --- .../Database/Eloquent/PartialRelation.php | 25 ------------------- .../Relations/Concerns/CanBeOneOfMany.php | 2 +- .../Concerns/ComparesRelatedModels.php | 4 +-- .../Database/Eloquent/Relations/HasOne.php | 6 ++--- 4 files changed, 6 insertions(+), 31 deletions(-) delete mode 100644 src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php diff --git a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php b/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php deleted file mode 100644 index 8e2191b3c318..000000000000 --- a/src/Illuminate/Contracts/Database/Eloquent/PartialRelation.php +++ /dev/null @@ -1,25 +0,0 @@ -related->getTable() === $model->getTable() && $this->related->getConnectionName() === $model->getConnectionName(); - if ($match && $this instanceof PartialRelation && $this->isOneOfMany()) { + if ($match && $this instanceof SupportsPartialRelations && $this->isOneOfMany()) { return $this->query ->whereKey($model->getKey()) ->exists(); diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index 17df79dc28d8..d4c0556dac20 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -2,7 +2,7 @@ namespace Illuminate\Database\Eloquent\Relations; -use Illuminate\Contracts\Database\Eloquent\PartialRelation; +use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; @@ -10,9 +10,9 @@ use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; -class HasOne extends HasOneOrMany implements PartialRelation +class HasOne extends HasOneOrMany implements SupportsPartialRelations { - use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels; + use ComparesRelatedModels, CanBeOneOfMany, SupportsDefaultModels; /** * Get the results of the relationship. From 47cccf7289228f01eb47abd435869b5360d2c821 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 14 May 2021 16:29:21 -0500 Subject: [PATCH 22/39] add file --- .../Eloquent/SupportsPartialRelations.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Illuminate/Contracts/Database/Eloquent/SupportsPartialRelations.php diff --git a/src/Illuminate/Contracts/Database/Eloquent/SupportsPartialRelations.php b/src/Illuminate/Contracts/Database/Eloquent/SupportsPartialRelations.php new file mode 100644 index 000000000000..a54b74d29b4a --- /dev/null +++ b/src/Illuminate/Contracts/Database/Eloquent/SupportsPartialRelations.php @@ -0,0 +1,25 @@ + Date: Fri, 14 May 2021 16:45:46 -0500 Subject: [PATCH 23/39] rename array key --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 6853586a59e7..438684f093ff 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -54,7 +54,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) ); if (isset($previous)) { - $this->addJoinSub($subQuery, $previous['sub'], $previous['column']); + $this->addJoinSub($subQuery, $previous['subQuery'], $previous['column']); } elseif (isset($closure)) { $closure($subQuery); } @@ -64,7 +64,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) } $previous = [ - 'sub' => $subQuery, + 'subQuery' => $subQuery, 'column' => $column, ]; } From 5ac60a866f8292ed8dfc1b097189cc72749405d5 Mon Sep 17 00:00:00 2001 From: cbl Date: Fri, 14 May 2021 23:50:56 +0200 Subject: [PATCH 24/39] add of-many to morph-one --- .../Relations/Concerns/CanBeOneOfMany.php | 47 +++++- .../Database/Eloquent/Relations/HasOne.php | 35 ++++ .../Database/Eloquent/Relations/MorphOne.php | 59 ++++++- .../DatabaseEloquentMorphOneOfManyTest.php | 159 ++++++++++++++++++ 4 files changed, 291 insertions(+), 9 deletions(-) create mode 100644 tests/Database/DatabaseEloquentMorphOneOfManyTest.php diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 6853586a59e7..0e378e9e81a6 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -4,6 +4,8 @@ use Closure; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Arr; use Illuminate\Support\Str; trait CanBeOneOfMany @@ -22,6 +24,31 @@ trait CanBeOneOfMany */ protected $relationName; + /** + * Add join sub constraints. + * + * @param JoinClause $join + * @return void + */ + abstract public function addJoinSubConstraints(JoinClause $join); + + /** + * Get the columns the determine the relationship groups. + * + * @return array|string + */ + abstract public function getGroups(); + + /** + * Add constraints for inner join subselect. + * + * @param Builder $query + * @param string|null $column + * @param string|null $aggregate + * @return void + */ + abstract public function addSubQueryConstraints(Builder $query, $column = null, $aggregate = null); + /** * Indicate that the relation is a single result of a larger one-to-many relationship. * @@ -49,7 +76,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) foreach ($columns as $column => $aggregate) { $subQuery = $this->newSubQuery( - isset($previous) ? $previous['column'] : $this->foreignKey, + isset($previous) ? $previous['column'] : $this->getGroups(), $column, $aggregate ); @@ -75,7 +102,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) /** * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship. * - * @param string $groupBy + * @param string|array $groupBy * @param string|null $column * @param string|null $aggregate * @return void @@ -83,13 +110,18 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) protected function newSubQuery($groupBy, $column = null, $aggregate = null) { $subQuery = $this->query->getModel() - ->newQuery() - ->groupBy($this->qualifyRelatedColumn($groupBy)); + ->newQuery(); + + foreach (Arr::wrap($groupBy) as $group) { + $subQuery->groupBy($this->qualifyRelatedColumn($group)); + } if (! is_null($column)) { - $subQuery->selectRaw($aggregate.'('.$column.') as '.$column.', '.$this->foreignKey); + $subQuery->selectRaw($aggregate.'('.$column.') as '.$column); } + $this->addSubQueryConstraints($subQuery, $groupBy, $column, $aggregate); + return $subQuery; } @@ -104,8 +136,9 @@ protected function newSubQuery($groupBy, $column = null, $aggregate = null) protected function addJoinSub(Builder $parent, Builder $subQuery, $on) { $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { - $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)) - ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); + + $this->addJoinSubConstraints($join, $on); }); } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index d4c0556dac20..f61bc910e921 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany; use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; +use Illuminate\Database\Query\JoinClause; class HasOne extends HasOneOrMany implements SupportsPartialRelations { @@ -103,4 +104,38 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $this->getQualifiedParentKeyName(), '=', $this->getExistenceCompareKey() ); } + + /** + * Add join sub constraints. + * + * @param JoinClause $join + * @return void + */ + public function addJoinSubConstraints(JoinClause $join) + { + $join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + } + + /** + * Add constraints for inner join subselect. + * + * @param Builder $query + * @param string|null $column + * @param string|null $aggregate + * @return void + */ + public function addSubQueryConstraints(Builder $query, $column = null, $aggregate = null) + { + $query->addSelect($this->foreignKey); + } + + /** + * Get the columns the determine the relationship groups. + * + * @return array|string + */ + public function getGroups() + { + return $this->foreignKey; + } } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOne.php b/src/Illuminate/Database/Eloquent/Relations/MorphOne.php index a874cdaec8d6..c4d6cc50d8a5 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOne.php @@ -2,14 +2,18 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Contracts\Database\Eloquent\SupportsPartialRelations; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Concerns\CanBeOneOfMany; use Illuminate\Database\Eloquent\Relations\Concerns\ComparesRelatedModels; use Illuminate\Database\Eloquent\Relations\Concerns\SupportsDefaultModels; +use Illuminate\Database\Query\JoinClause; -class MorphOne extends MorphOneOrMany +class MorphOne extends MorphOneOrMany implements SupportsPartialRelations { - use ComparesRelatedModels, SupportsDefaultModels; + use CanBeOneOfMany, ComparesRelatedModels, SupportsDefaultModels; /** * Get the results of the relationship. @@ -54,6 +58,21 @@ public function match(array $models, Collection $results, $relation) return $this->matchOne($models, $results, $relation); } + /** + * Get the relationship query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $parentQuery + * @param array|mixed $columns + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) + { + $query->getQuery()->joins = $this->query->getQuery()->joins; + + return parent::getRelationExistenceQuery($query, $parentQuery, $columns); + } + /** * Make a new related instance for the given model. * @@ -77,4 +96,40 @@ protected function getRelatedKeyFrom(Model $model) { return $model->getAttribute($this->getForeignKeyName()); } + + /** + * Add join sub constraints. + * + * @param JoinClause $join + * @return void + */ + public function addJoinSubConstraints(JoinClause $join) + { + $join + ->on($this->qualifySubSelectColumn($this->morphType), '=', $this->qualifyRelatedColumn($this->morphType)) + ->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + } + + /** + * Add constraints for inner join subselect. + * + * @param Builder $query + * @param string|null $column + * @param string|null $aggregate + * @return void + */ + public function addSubQueryConstraints(Builder $query, $column = null, $aggregate = null) + { + $query->addSelect($this->foreignKey, $this->morphType); + } + + /** + * Get the columns the determine the relationship groups. + * + * @return array|string + */ + public function getGroups() + { + return [$this->foreignKey, $this->morphType]; + } } diff --git a/tests/Database/DatabaseEloquentMorphOneOfManyTest.php b/tests/Database/DatabaseEloquentMorphOneOfManyTest.php new file mode 100644 index 000000000000..9d514f60a4cc --- /dev/null +++ b/tests/Database/DatabaseEloquentMorphOneOfManyTest.php @@ -0,0 +1,159 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + + $this->createSchema(); + } + + /** + * Setup the database schema. + * + * @return void + */ + public function createSchema() + { + $this->schema()->create('products', function ($table) { + $table->increments('id'); + }); + + $this->schema()->create('states', function ($table) { + $table->increments('id'); + $table->morphs('stateful'); + $table->string('state'); + }); + } + + /** + * Tear down the database schema. + * + * @return void + */ + protected function tearDown(): void + { + $this->schema()->drop('products'); + $this->schema()->drop('states'); + } + + public function testReceivingModel() + { + $product = MorphOneOfManyTestProduct::create(); + $product->states()->create([ + 'state' => 'draft' + ]); + $product->states()->create([ + 'state' => 'active' + ]); + + $this->assertNotNull($product->current_state); + $this->assertSame('active', $product->current_state->state); + } + + public function testMorphType() + { + $product = MorphOneOfManyTestProduct::create(); + $product->states()->create([ + 'state' => 'draft' + ]); + $product->states()->create([ + 'state' => 'active' + ]); + $state = $product->states()->make([ + 'state' => 'foo' + ]); + $state->stateful_type = 'bar'; + $state->save(); + + $this->assertNotNull($product->current_state); + $this->assertSame('active', $product->current_state->state); + } + + public function testExists() + { + $product = MorphOneOfManyTestProduct::create(); + $previousState = $product->states()->create([ + 'state' => 'draft' + ]); + $currentState = $product->states()->create([ + 'state' => 'active' + ]); + + $exists = MorphOneOfManyTestProduct::whereHas('current_state', function($q) use($previousState){ + $q->whereKey($previousState->getKey()); + })->exists(); + $this->assertFalse($exists); + + $exists = MorphOneOfManyTestProduct::whereHas('current_state', function($q) use($currentState){ + $q->whereKey($currentState->getKey()); + })->exists(); + $this->assertTrue($exists); + } + + /** + * Get a database connection instance. + * + * @return \Illuminate\Database\Connection + */ + protected function connection() + { + return Eloquent::getConnectionResolver()->connection(); + } + + /** + * Get a schema builder instance. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function schema() + { + return $this->connection()->getSchemaBuilder(); + } +} + +/** + * Eloquent Models... + */ +class MorphOneOfManyTestProduct extends Eloquent +{ + protected $table = 'products'; + protected $guarded = []; + public $timestamps = false; + + public function states() + { + return $this->morphMany(MorphOneOfManyTestState::class, 'stateful'); + } + + public function current_state() + { + return $this->morphOne(MorphOneOfManyTestState::class, 'stateful')->ofMany(); + } +} + +class MorphOneOfManyTestState extends Eloquent +{ + protected $table = 'states'; + protected $guarded = []; + public $timestamps = false; + protected $fillable = ['state']; +} From a81c9a58118c5b4ef4194eb2b4f950242a7ff07c Mon Sep 17 00:00:00 2001 From: cbl Date: Fri, 14 May 2021 23:52:02 +0200 Subject: [PATCH 25/39] Apply fixes from StyleCI --- .../DatabaseEloquentMorphOneOfManyTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Database/DatabaseEloquentMorphOneOfManyTest.php b/tests/Database/DatabaseEloquentMorphOneOfManyTest.php index 9d514f60a4cc..85ab66d8074d 100644 --- a/tests/Database/DatabaseEloquentMorphOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentMorphOneOfManyTest.php @@ -59,10 +59,10 @@ public function testReceivingModel() { $product = MorphOneOfManyTestProduct::create(); $product->states()->create([ - 'state' => 'draft' + 'state' => 'draft', ]); $product->states()->create([ - 'state' => 'active' + 'state' => 'active', ]); $this->assertNotNull($product->current_state); @@ -73,13 +73,13 @@ public function testMorphType() { $product = MorphOneOfManyTestProduct::create(); $product->states()->create([ - 'state' => 'draft' + 'state' => 'draft', ]); $product->states()->create([ - 'state' => 'active' + 'state' => 'active', ]); $state = $product->states()->make([ - 'state' => 'foo' + 'state' => 'foo', ]); $state->stateful_type = 'bar'; $state->save(); @@ -92,18 +92,18 @@ public function testExists() { $product = MorphOneOfManyTestProduct::create(); $previousState = $product->states()->create([ - 'state' => 'draft' + 'state' => 'draft', ]); $currentState = $product->states()->create([ - 'state' => 'active' + 'state' => 'active', ]); - $exists = MorphOneOfManyTestProduct::whereHas('current_state', function($q) use($previousState){ + $exists = MorphOneOfManyTestProduct::whereHas('current_state', function ($q) use ($previousState) { $q->whereKey($previousState->getKey()); })->exists(); $this->assertFalse($exists); - $exists = MorphOneOfManyTestProduct::whereHas('current_state', function($q) use($currentState){ + $exists = MorphOneOfManyTestProduct::whereHas('current_state', function ($q) use ($currentState) { $q->whereKey($currentState->getKey()); })->exists(); $this->assertTrue($exists); From 5b5a5c12e2a039569c97ae820e676fb2fa530436 Mon Sep 17 00:00:00 2001 From: cbl Date: Sat, 15 May 2021 01:05:40 +0200 Subject: [PATCH 26/39] fixed pivot test --- tests/Database/DatabaseEloquentPivotTest.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/Database/DatabaseEloquentPivotTest.php b/tests/Database/DatabaseEloquentPivotTest.php index 50beacb588da..510887faf414 100755 --- a/tests/Database/DatabaseEloquentPivotTest.php +++ b/tests/Database/DatabaseEloquentPivotTest.php @@ -2,11 +2,15 @@ namespace Illuminate\Tests\Database; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\Pivot; +use stdClass; use Mockery as m; use PHPUnit\Framework\TestCase; -use stdClass; +use Illuminate\Database\Connection; +use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Database\Query\Grammars\Grammar; +use Illuminate\Database\Query\Processors\Processor; class DatabaseEloquentPivotTest extends TestCase { @@ -19,6 +23,10 @@ public function testPropertiesAreSetCorrectly() { $parent = m::mock(Model::class.'[getConnectionName]'); $parent->shouldReceive('getConnectionName')->twice()->andReturn('connection'); + $parent->setConnectionResolver($resolver = m::mock(ConnectionResolverInterface::class)); + $resolver->shouldReceive('connection')->andReturn($connection = m::mock(Connection::class)); + $connection->shouldReceive('getQueryGrammar')->andReturn($grammar = m::mock(Grammar::class)); + $connection->shouldReceive('getPostProcessor')->andReturn($processor = m::mock(Processor::class)); $parent->getConnection()->getQueryGrammar()->shouldReceive('getDateFormat')->andReturn('Y-m-d H:i:s'); $parent->setDateFormat('Y-m-d H:i:s'); $pivot = Pivot::fromAttributes($parent, ['foo' => 'bar', 'created_at' => '2015-09-12'], 'table', true); From b9c3d98db7b21a8602acf2dbfe9957603710fcee Mon Sep 17 00:00:00 2001 From: cbl Date: Sat, 15 May 2021 01:06:33 +0200 Subject: [PATCH 27/39] Apply fixes from StyleCI --- tests/Database/DatabaseEloquentPivotTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Database/DatabaseEloquentPivotTest.php b/tests/Database/DatabaseEloquentPivotTest.php index 510887faf414..ad774d7c1a68 100755 --- a/tests/Database/DatabaseEloquentPivotTest.php +++ b/tests/Database/DatabaseEloquentPivotTest.php @@ -2,15 +2,15 @@ namespace Illuminate\Tests\Database; -use stdClass; -use Mockery as m; -use PHPUnit\Framework\TestCase; use Illuminate\Database\Connection; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Database\Query\Grammars\Grammar; use Illuminate\Database\Query\Processors\Processor; +use Mockery as m; +use PHPUnit\Framework\TestCase; +use stdClass; class DatabaseEloquentPivotTest extends TestCase { From 85c1e2d51b50f9807a93cfd66267c405a9c1b7a4 Mon Sep 17 00:00:00 2001 From: Lennart Carstens-Behrens Date: Sat, 15 May 2021 15:03:38 +0200 Subject: [PATCH 28/39] fixed return type --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 3e2d99eb5fa5..812388a3c958 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -105,7 +105,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) * @param string|array $groupBy * @param string|null $column * @param string|null $aggregate - * @return void + * @return \Illuminate\Database\Eloquent\Builder */ protected function newSubQuery($groupBy, $column = null, $aggregate = null) { From 6e1997f76d4aa0bfd345a22ad71e1c22919ccc0e Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 17 May 2021 13:43:44 -0500 Subject: [PATCH 29/39] formatting --- .../Relations/Concerns/CanBeOneOfMany.php | 24 +++--- .../Database/Eloquent/Relations/HasOne.php | 76 +++++++++---------- .../Database/Eloquent/Relations/MorphOne.php | 52 ++++++------- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 812388a3c958..ebe08b964d50 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -25,29 +25,29 @@ trait CanBeOneOfMany protected $relationName; /** - * Add join sub constraints. + * Add constraints for inner join subselect for one of many relationships. * - * @param JoinClause $join + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|null $column + * @param string|null $aggregate * @return void */ - abstract public function addJoinSubConstraints(JoinClause $join); + abstract public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null); /** * Get the columns the determine the relationship groups. * * @return array|string */ - abstract public function getGroups(); + abstract public function getOneOfManySubQuerySelectColumns(); /** - * Add constraints for inner join subselect. + * Add join query constraints for one of many relationships. * - * @param Builder $query - * @param string|null $column - * @param string|null $aggregate + * @param \Illuminate\Database\Eloquent\JoinClause $join * @return void */ - abstract public function addSubQueryConstraints(Builder $query, $column = null, $aggregate = null); + abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join); /** * Indicate that the relation is a single result of a larger one-to-many relationship. @@ -76,7 +76,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) foreach ($columns as $column => $aggregate) { $subQuery = $this->newSubQuery( - isset($previous) ? $previous['column'] : $this->getGroups(), + isset($previous) ? $previous['column'] : $this->getOneOfManySubQuerySelectColumns(), $column, $aggregate ); @@ -120,7 +120,7 @@ protected function newSubQuery($groupBy, $column = null, $aggregate = null) $subQuery->selectRaw($aggregate.'('.$column.') as '.$column); } - $this->addSubQueryConstraints($subQuery, $groupBy, $column, $aggregate); + $this->addOneOfManySubQueryConstraints($subQuery, $groupBy, $column, $aggregate); return $subQuery; } @@ -138,7 +138,7 @@ protected function addJoinSub(Builder $parent, Builder $subQuery, $on) $parent->joinSub($subQuery, $this->relationName, function ($join) use ($on) { $join->on($this->qualifySubSelectColumn($on), '=', $this->qualifyRelatedColumn($on)); - $this->addJoinSubConstraints($join, $on); + $this->addOneOfManyJoinSubQueryConstraints($join, $on); }); } diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOne.php b/src/Illuminate/Database/Eloquent/Relations/HasOne.php index f61bc910e921..dc4ee3fd338c 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOne.php @@ -58,30 +58,6 @@ public function match(array $models, Collection $results, $relation) return $this->matchOne($models, $results, $relation); } - /** - * Make a new related instance for the given model. - * - * @param \Illuminate\Database\Eloquent\Model $parent - * @return \Illuminate\Database\Eloquent\Model - */ - public function newRelatedInstanceFor(Model $parent) - { - return $this->related->newInstance()->setAttribute( - $this->getForeignKeyName(), $parent->{$this->localKey} - ); - } - - /** - * Get the value of the model's foreign key. - * - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed - */ - protected function getRelatedKeyFrom(Model $model) - { - return $model->getAttribute($this->getForeignKeyName()); - } - /** * Add the constraints for an internal relationship existence query. * @@ -106,36 +82,60 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, } /** - * Add join sub constraints. + * Add constraints for inner join subselect for one of many relationships. * - * @param JoinClause $join + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|null $column + * @param string|null $aggregate * @return void */ - public function addJoinSubConstraints(JoinClause $join) + public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null) { - $join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); + $query->addSelect($this->foreignKey); } /** - * Add constraints for inner join subselect. + * Get the columns that should be selected by the one of many subquery. * - * @param Builder $query - * @param string|null $column - * @param string|null $aggregate + * @return array|string + */ + public function getOneOfManySubQuerySelectColumns() + { + return $this->foreignKey; + } + + /** + * Add join query constraints for one of many relationships. + * + * @param \Illuminate\Database\Eloquent\JoinClause $join * @return void */ - public function addSubQueryConstraints(Builder $query, $column = null, $aggregate = null) + public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) { - $query->addSelect($this->foreignKey); + $join->on($this->qualifySubSelectColumn($this->foreignKey), '=', $this->qualifyRelatedColumn($this->foreignKey)); } /** - * Get the columns the determine the relationship groups. + * Make a new related instance for the given model. * - * @return array|string + * @param \Illuminate\Database\Eloquent\Model $parent + * @return \Illuminate\Database\Eloquent\Model */ - public function getGroups() + public function newRelatedInstanceFor(Model $parent) { - return $this->foreignKey; + return $this->related->newInstance()->setAttribute( + $this->getForeignKeyName(), $parent->{$this->localKey} + ); + } + + /** + * Get the value of the model's foreign key. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed + */ + protected function getRelatedKeyFrom(Model $model) + { + return $model->getAttribute($this->getForeignKeyName()); } } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphOne.php b/src/Illuminate/Database/Eloquent/Relations/MorphOne.php index c4d6cc50d8a5..7a3353cbe498 100755 --- a/src/Illuminate/Database/Eloquent/Relations/MorphOne.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphOne.php @@ -74,36 +74,35 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, } /** - * Make a new related instance for the given model. + * Add constraints for inner join subselect for one of many relationships. * - * @param \Illuminate\Database\Eloquent\Model $parent - * @return \Illuminate\Database\Eloquent\Model + * @param \Illuminate\Database\Eloquent\Builder $query + * @param string|null $column + * @param string|null $aggregate + * @return void */ - public function newRelatedInstanceFor(Model $parent) + public function addOneOfManySubQueryConstraints(Builder $query, $column = null, $aggregate = null) { - return $this->related->newInstance() - ->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}) - ->setAttribute($this->getMorphType(), $this->morphClass); + $query->addSelect($this->foreignKey, $this->morphType); } /** - * Get the value of the model's foreign key. + * Get the columns that should be selected by the one of many subquery. * - * @param \Illuminate\Database\Eloquent\Model $model - * @return mixed + * @return array|string */ - protected function getRelatedKeyFrom(Model $model) + public function getOneOfManySubQuerySelectColumns() { - return $model->getAttribute($this->getForeignKeyName()); + return [$this->foreignKey, $this->morphType]; } /** - * Add join sub constraints. + * Add join query constraints for one of many relationships. * - * @param JoinClause $join + * @param \Illuminate\Database\Eloquent\JoinClause $join * @return void */ - public function addJoinSubConstraints(JoinClause $join) + public function addOneOfManyJoinSubQueryConstraints(JoinClause $join) { $join ->on($this->qualifySubSelectColumn($this->morphType), '=', $this->qualifyRelatedColumn($this->morphType)) @@ -111,25 +110,26 @@ public function addJoinSubConstraints(JoinClause $join) } /** - * Add constraints for inner join subselect. + * Make a new related instance for the given model. * - * @param Builder $query - * @param string|null $column - * @param string|null $aggregate - * @return void + * @param \Illuminate\Database\Eloquent\Model $parent + * @return \Illuminate\Database\Eloquent\Model */ - public function addSubQueryConstraints(Builder $query, $column = null, $aggregate = null) + public function newRelatedInstanceFor(Model $parent) { - $query->addSelect($this->foreignKey, $this->morphType); + return $this->related->newInstance() + ->setAttribute($this->getForeignKeyName(), $parent->{$this->localKey}) + ->setAttribute($this->getMorphType(), $this->morphClass); } /** - * Get the columns the determine the relationship groups. + * Get the value of the model's foreign key. * - * @return array|string + * @param \Illuminate\Database\Eloquent\Model $model + * @return mixed */ - public function getGroups() + protected function getRelatedKeyFrom(Model $model) { - return [$this->foreignKey, $this->morphType]; + return $model->getAttribute($this->getForeignKeyName()); } } From e32edb6446480a267dac81fdc023c1dc3ad53b0d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 17 May 2021 14:00:23 -0500 Subject: [PATCH 30/39] add shortcut methods --- .../Relations/Concerns/CanBeOneOfMany.php | 26 +++++++++++++++++ .../Database/EloquentHasOneOfManyTest.php | 28 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index ebe08b964d50..10be826c8267 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -99,6 +99,32 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) return $this; } + /** + * Indicate that the relation is the latest single result of a larger one-to-many relationship. + * + * @param string|array|null $column + * @param string|Closure|null $aggregate + * @param string|null $relation + * @return $this + */ + public function latestOfMany($column = 'id', $relation = null) + { + return $this->ofMany($column, 'MAX', $relation ?: $this->guessRelationship()); + } + + /** + * Indicate that the relation is the oldest single result of a larger one-to-many relationship. + * + * @param string|array|null $column + * @param string|Closure|null $aggregate + * @param string|null $relation + * @return $this + */ + public function oldestOfMany($column = 'id', $relation = null) + { + return $this->ofMany($column, 'MIN', $relation ?: $this->guessRelationship()); + } + /** * Get a new query for the related model, grouping the query by the given column, often the foreign key of the relationship. * diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php index 96c81eb27f76..3ca301d6ac49 100644 --- a/tests/Integration/Database/EloquentHasOneOfManyTest.php +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -54,6 +54,29 @@ public function testItOnlyEagerLoadsRequiredModels() $this->assertSame(2, $this->retrievedLogins); } + + public function testItOnlyEagerLoadsRequiredModelsUsingShortcutMethod() + { + $this->retrievedLogins = 0; + User::getEventDispatcher()->listen('eloquent.retrieved:*', function ($event, $models) { + foreach ($models as $model) { + if (get_class($model) == Login::class) { + $this->retrievedLogins++; + } + } + }); + + $user = User::create(); + $user->latest_login_shortcut()->create(); + $user->latest_login_shortcut()->create(); + $user = User::create(); + $user->latest_login_shortcut()->create(); + $user->latest_login_shortcut()->create(); + + User::with('latest_login_shortcut')->get(); + + $this->assertSame(2, $this->retrievedLogins); + } } class User extends Model @@ -65,6 +88,11 @@ public function latest_login() { return $this->hasOne(Login::class)->ofMany(); } + + public function latest_login_shortcut() + { + return $this->hasOne(Login::class)->latestOfMany(); + } } class Login extends Model From 159db7110b82eb2c2ae065ab394cd2951b2dd1de Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 17 May 2021 14:15:35 -0500 Subject: [PATCH 31/39] move test --- .../DatabaseEloquentHasOneOfManyTest.php | 16 +++++++++++ .../Database/EloquentHasOneOfManyTest.php | 28 ------------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 715c0e1806dd..13a2f79e72d2 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -96,6 +96,17 @@ public function testItGetsCorrectResults() $this->assertSame($latestLogin->id, $result->id); } + public function testItGetsCorrectResultsUsingShortcutMethod() + { + $user = HasOneOfManyTestUser::create(); + $previousLogin = $user->logins()->create(); + $latestLogin = $user->logins()->create(); + + $result = $user->latest_login_with_shortcut()->getResults(); + $this->assertNotNull($result); + $this->assertSame($latestLogin->id, $result->id); + } + public function testItGetsWithConstraintsCorrectResults() { $user = HasOneOfManyTestUser::create(); @@ -286,6 +297,11 @@ public function latest_login() return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany(); } + public function latest_login_with_shortcut() + { + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->latestOfMany(); + } + public function first_login() { return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany('id', 'min'); diff --git a/tests/Integration/Database/EloquentHasOneOfManyTest.php b/tests/Integration/Database/EloquentHasOneOfManyTest.php index 3ca301d6ac49..96c81eb27f76 100644 --- a/tests/Integration/Database/EloquentHasOneOfManyTest.php +++ b/tests/Integration/Database/EloquentHasOneOfManyTest.php @@ -54,29 +54,6 @@ public function testItOnlyEagerLoadsRequiredModels() $this->assertSame(2, $this->retrievedLogins); } - - public function testItOnlyEagerLoadsRequiredModelsUsingShortcutMethod() - { - $this->retrievedLogins = 0; - User::getEventDispatcher()->listen('eloquent.retrieved:*', function ($event, $models) { - foreach ($models as $model) { - if (get_class($model) == Login::class) { - $this->retrievedLogins++; - } - } - }); - - $user = User::create(); - $user->latest_login_shortcut()->create(); - $user->latest_login_shortcut()->create(); - $user = User::create(); - $user->latest_login_shortcut()->create(); - $user->latest_login_shortcut()->create(); - - User::with('latest_login_shortcut')->get(); - - $this->assertSame(2, $this->retrievedLogins); - } } class User extends Model @@ -88,11 +65,6 @@ public function latest_login() { return $this->hasOne(Login::class)->ofMany(); } - - public function latest_login_shortcut() - { - return $this->hasOne(Login::class)->latestOfMany(); - } } class Login extends Model From d12c20e091fffbd008b76ffa904d4bbfe7648fb8 Mon Sep 17 00:00:00 2001 From: cbl Date: Mon, 17 May 2021 21:31:00 +0200 Subject: [PATCH 32/39] multiple columns in shortcut --- .../Relations/Concerns/CanBeOneOfMany.php | 16 +++++++++++++-- .../DatabaseEloquentHasOneOfManyTest.php | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 10be826c8267..9842c287ba04 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -109,7 +109,13 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) */ public function latestOfMany($column = 'id', $relation = null) { - return $this->ofMany($column, 'MAX', $relation ?: $this->guessRelationship()); + $aggregates = []; + + foreach(Arr::wrap($column) as $col) { + $aggregates[$col] = 'MAX'; + } + + return $this->ofMany($aggregates, 'MAX', $relation ?: $this->guessRelationship()); } /** @@ -122,7 +128,13 @@ public function latestOfMany($column = 'id', $relation = null) */ public function oldestOfMany($column = 'id', $relation = null) { - return $this->ofMany($column, 'MIN', $relation ?: $this->guessRelationship()); + $aggregates = []; + + foreach(Arr::wrap($column) as $col) { + $aggregates[$col] = 'MIN'; + } + + return $this->ofMany($aggregates, 'MIN', $relation ?: $this->guessRelationship()); } /** diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 13a2f79e72d2..d6e73f670727 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -107,6 +107,21 @@ public function testItGetsCorrectResultsUsingShortcutMethod() $this->assertSame($latestLogin->id, $result->id); } + public function testItGetsCorrectResultsUsingShortcutReceivingMultipleColumnsMethod() + { + $user = HasOneOfManyTestUser::create(); + $user->prices()->create([ + 'published_at' => '2021-05-01 00:00:00', + ]); + $price = $user->prices()->create([ + 'published_at' => '2021-05-01 00:00:00', + ]); + + $result = $user->price_with_shortcut()->getResults(); + $this->assertNotNull($result); + $this->assertSame($price->id, $result->id); + } + public function testItGetsWithConstraintsCorrectResults() { $user = HasOneOfManyTestUser::create(); @@ -336,6 +351,11 @@ public function price() $q->where('published_at', '<', now()); }); } + + public function price_with_shortcut() + { + return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->latestOfMany(['published_at', 'id']); + } } class HasOneOfManyTestLogin extends Eloquent From 1a8859828c2700cd01a587a1cc75622e12f60857 Mon Sep 17 00:00:00 2001 From: cbl Date: Mon, 17 May 2021 21:33:09 +0200 Subject: [PATCH 33/39] Apply fixes from StyleCI --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 9842c287ba04..28e6c1cc8fa9 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -111,7 +111,7 @@ public function latestOfMany($column = 'id', $relation = null) { $aggregates = []; - foreach(Arr::wrap($column) as $col) { + foreach (Arr::wrap($column) as $col) { $aggregates[$col] = 'MAX'; } @@ -130,7 +130,7 @@ public function oldestOfMany($column = 'id', $relation = null) { $aggregates = []; - foreach(Arr::wrap($column) as $col) { + foreach (Arr::wrap($column) as $col) { $aggregates[$col] = 'MIN'; } From bc5334b14e4f8802401420ccae091967c446050c Mon Sep 17 00:00:00 2001 From: cbl Date: Mon, 17 May 2021 21:47:23 +0200 Subject: [PATCH 34/39] add key when missing --- .../Relations/Concerns/CanBeOneOfMany.php | 4 ++++ .../DatabaseEloquentHasOneOfManyTest.php | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 28e6c1cc8fa9..fd4b13dd46cc 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -70,6 +70,10 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) $keyName => $aggregate, ] : $column; + if(! array_key_exists($keyName, $columns)) { + $columns[$keyName] = 'MAX'; + } + if ($aggregate instanceof Closure) { $closure = $aggregate; } diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index d6e73f670727..7fb494a0d0db 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -122,6 +122,21 @@ public function testItGetsCorrectResultsUsingShortcutReceivingMultipleColumnsMet $this->assertSame($price->id, $result->id); } + public function testKeyIsAddedToAggregatesWhenMissing() + { + $user = HasOneOfManyTestUser::create(); + $user->prices()->create([ + 'published_at' => '2021-05-01 00:00:00', + ]); + $price = $user->prices()->create([ + 'published_at' => '2021-05-01 00:00:00', + ]); + + $result = $user->price_without_key_in_aggregates()->getResults(); + $this->assertNotNull($result); + $this->assertSame($price->id, $result->id); + } + public function testItGetsWithConstraintsCorrectResults() { $user = HasOneOfManyTestUser::create(); @@ -352,6 +367,11 @@ public function price() }); } + public function price_without_key_in_aggregates() + { + return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->ofMany(['published_at' => 'MAX']); + } + public function price_with_shortcut() { return $this->hasOne(HasOneOfManyTestPrice::class, 'user_id')->latestOfMany(['published_at', 'id']); From 390ec52c8575bf14e0808fa9e5b67fc908ec107c Mon Sep 17 00:00:00 2001 From: cbl Date: Mon, 17 May 2021 21:48:41 +0200 Subject: [PATCH 35/39] Apply fixes from StyleCI --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index fd4b13dd46cc..05b1b74183cb 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -70,7 +70,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) $keyName => $aggregate, ] : $column; - if(! array_key_exists($keyName, $columns)) { + if (! array_key_exists($keyName, $columns)) { $columns[$keyName] = 'MAX'; } From 6a8f16f33e75aa1c78f7ec33d35468623f46e57e Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 17 May 2021 14:55:57 -0500 Subject: [PATCH 36/39] use collections --- .../Relations/Concerns/CanBeOneOfMany.php | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 05b1b74183cb..10807785cd1c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -113,13 +113,9 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) */ public function latestOfMany($column = 'id', $relation = null) { - $aggregates = []; - - foreach (Arr::wrap($column) as $col) { - $aggregates[$col] = 'MAX'; - } - - return $this->ofMany($aggregates, 'MAX', $relation ?: $this->guessRelationship()); + return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) { + return [$column => 'MAX']; + })->all(), 'MAX', $relation ?: $this->guessRelationship()); } /** @@ -132,13 +128,9 @@ public function latestOfMany($column = 'id', $relation = null) */ public function oldestOfMany($column = 'id', $relation = null) { - $aggregates = []; - - foreach (Arr::wrap($column) as $col) { - $aggregates[$col] = 'MIN'; - } - - return $this->ofMany($aggregates, 'MIN', $relation ?: $this->guessRelationship()); + return $this->ofMany(collect(Arr::wrap($column))->mapWithKeys(function ($column) { + return [$column => 'MIN']; + })->all(), 'MIN', $relation ?: $this->guessRelationship()); } /** From 5ae44e35a47bd874dcf0fa0c303eee41460d4c15 Mon Sep 17 00:00:00 2001 From: cbl Date: Mon, 17 May 2021 22:13:57 +0200 Subject: [PATCH 37/39] fail for invalid aggregates --- .../Eloquent/Relations/Concerns/CanBeOneOfMany.php | 7 +++++++ .../Database/DatabaseEloquentHasOneOfManyTest.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 05b1b74183cb..3356a27acbd8 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -7,6 +7,7 @@ use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use InvalidArgumentException; trait CanBeOneOfMany { @@ -56,6 +57,8 @@ abstract public function addOneOfManyJoinSubQueryConstraints(JoinClause $join); * @param string|Closure|null $aggregate * @param string|null $relation * @return $this + * + * @throws \InvalidArgumentException */ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) { @@ -79,6 +82,10 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) } foreach ($columns as $column => $aggregate) { + if(! in_array(strtolower($aggregate), ['min', 'max'])) { + throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] to use in ofMany. Available aggregates: MIN, MAX"); + } + $subQuery = $this->newSubQuery( isset($previous) ? $previous['column'] : $this->getOneOfManySubQuerySelectColumns(), $column, $aggregate diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 7fb494a0d0db..504af84dd1ef 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Capsule\Manager as DB; use Illuminate\Database\Eloquent\Model as Eloquent; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -85,6 +86,14 @@ public function testQualifyingSubSelectColumn() $this->assertSame('latest_login.id', $user->latest_login()->qualifySubSelectColumn('id')); } + public function testItFailsWhenUsingInvalidAggregate() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Invalid aggregate [count] to use in ofMany. Available aggregates: MIN, MAX"); + $user = HasOneOfManyTestUser::make(); + $user->latest_login_with_invalid_aggregate(); + } + public function testItGetsCorrectResults() { $user = HasOneOfManyTestUser::create(); @@ -332,6 +341,11 @@ public function latest_login_with_shortcut() return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->latestOfMany(); } + public function latest_login_with_invalid_aggregate() + { + return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany('id', 'count'); + } + public function first_login() { return $this->hasOne(HasOneOfManyTestLogin::class, 'user_id')->ofMany('id', 'min'); From d6bf52619af9b5a26c4432e045eb47fc1df75f3c Mon Sep 17 00:00:00 2001 From: cbl Date: Mon, 17 May 2021 22:15:01 +0200 Subject: [PATCH 38/39] Apply fixes from StyleCI --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- tests/Database/DatabaseEloquentHasOneOfManyTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 0bd98852152c..24b5248b3ddf 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -82,7 +82,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) } foreach ($columns as $column => $aggregate) { - if(! in_array(strtolower($aggregate), ['min', 'max'])) { + if (! in_array(strtolower($aggregate), ['min', 'max'])) { throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] to use in ofMany. Available aggregates: MIN, MAX"); } diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 504af84dd1ef..690c43f178cc 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -89,7 +89,7 @@ public function testQualifyingSubSelectColumn() public function testItFailsWhenUsingInvalidAggregate() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Invalid aggregate [count] to use in ofMany. Available aggregates: MIN, MAX"); + $this->expectExceptionMessage('Invalid aggregate [count] to use in ofMany. Available aggregates: MIN, MAX'); $user = HasOneOfManyTestUser::make(); $user->latest_login_with_invalid_aggregate(); } From 3a6ebff0ec2d0507c45a2742128189883e57bb91 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 17 May 2021 15:22:25 -0500 Subject: [PATCH 39/39] formatting --- .../Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php | 2 +- tests/Database/DatabaseEloquentHasOneOfManyTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php index 24b5248b3ddf..ea1afb539c37 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/CanBeOneOfMany.php @@ -83,7 +83,7 @@ public function ofMany($column = 'id', $aggregate = 'MAX', $relation = null) foreach ($columns as $column => $aggregate) { if (! in_array(strtolower($aggregate), ['min', 'max'])) { - throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] to use in ofMany. Available aggregates: MIN, MAX"); + throw new InvalidArgumentException("Invalid aggregate [{$aggregate}] used within ofMany relation. Available aggregates: MIN, MAX"); } $subQuery = $this->newSubQuery( diff --git a/tests/Database/DatabaseEloquentHasOneOfManyTest.php b/tests/Database/DatabaseEloquentHasOneOfManyTest.php index 690c43f178cc..37d5925f382d 100755 --- a/tests/Database/DatabaseEloquentHasOneOfManyTest.php +++ b/tests/Database/DatabaseEloquentHasOneOfManyTest.php @@ -89,7 +89,7 @@ public function testQualifyingSubSelectColumn() public function testItFailsWhenUsingInvalidAggregate() { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid aggregate [count] to use in ofMany. Available aggregates: MIN, MAX'); + $this->expectExceptionMessage('Invalid aggregate [count] used within ofMany relation. Available aggregates: MIN, MAX'); $user = HasOneOfManyTestUser::make(); $user->latest_login_with_invalid_aggregate(); }