diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index c97c4033082e..f78cdf0ac8a2 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -348,12 +348,15 @@ public function orWhereDoesntHaveMorph($relation, $types, Closure $callback = nu } /** - * Add subselect queries to count the relations. + * Add subselect queries to include the given relation's column. + * Also, apply the SQL function on that column if provided. * - * @param mixed $relations + * @param mixed $relation + * @param string $column + * @param string $function * @return $this */ - public function withCount($relations) + public function withColumn($relations, $column, $function = null) { if (empty($relations)) { return $this; @@ -363,12 +366,12 @@ public function withCount($relations) $this->query->select([$this->query->from.'.*']); } - $relations = is_array($relations) ? $relations : func_get_args(); + $relations = is_array($relations) ? $relations : [$relations]; foreach ($this->parseWithRelations($relations) as $name => $constraints) { // First we will determine if the name has been aliased using an "as" clause on the name // and if it has we will extract the actual relationship name and the desired name of - // the resulting column. This allows multiple counts on the same relationship name. + // the resulting column. $segments = explode(' ', $name); unset($alias); @@ -379,12 +382,13 @@ public function withCount($relations) $relation = $this->getRelationWithoutConstraints($name); - // Here we will get the relationship count query and prepare to add it to the main query + // Here we will get the relationship sub-query and prepare to add it to the main query // as a sub-select. First, we'll get the "has" query and use that to get the relation - // count query. We will normalize the relation name then append _count as the name. - $query = $relation->getRelationExistenceCountQuery( - $relation->getRelated()->newQuery(), $this - ); + // sub-query. + // We will normalize the relation name then append _$column as the name if the $alias was not given. + $query = $relation->getRelationExistenceQuery( + $relation->getRelated()->newQuery(), $this, new Expression($function ? "$function($column)" : $column) + )->setBindings([], 'select'); $query->callScope($constraints); @@ -400,17 +404,76 @@ public function withCount($relations) $query->bindings['select'] = []; } - // Finally we will add the proper result column alias to the query and run the subselect + // Finally we will make the proper column alias to the query and run the subselect // statement against the query builder. Then we will return the builder instance back // to the developer for further constraint chaining that needs to take place on it. - $column = $alias ?? Str::snake($name.'_count'); + $alias = $alias ?? Str::snake(preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column")); - $this->selectSub($query, $column); + $this->selectSub($query->limit(1), $alias); } return $this; } + /** + * Add subselect queries to count the relations. + * + * @param mixed $relations + * @return $this + */ + public function withCount($relations) + { + return $this->withColumn(is_array($relations) ? $relations : func_get_args(), '*', 'count'); + } + + /** + * Add subselect queries to include the max of the relation's column. + * + * @param string $relation + * @param string $column + * @return $this + */ + public function withMax($relation, $column) + { + return $this->withColumn($relation, $column, 'max'); + } + + /** + * Add subselect queries to include the min of the relation's column. + * + * @param string $relation + * @param string $column + * @return $this + */ + public function withMin($relation, $column) + { + return $this->withColumn($relation, $column, 'min'); + } + + /** + * Add subselect queries to include the sum of the relation's column. + * + * @param string $relation + * @param string $column + * @return $this + */ + public function withSum($relation, $column) + { + return $this->withColumn($relation, $column, 'sum'); + } + + /** + * Add subselect queries to include the average of the relation's column. + * + * @param string $relation + * @param string $column + * @return $this + */ + public function withAvg($relation, $column) + { + return $this->withColumn($relation, $column, 'avg'); + } + /** * Add the "has" condition where clause to the query. * diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 70e6bb875206..a5253f1d293a 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -775,7 +775,7 @@ public function testWithCount() $builder = $model->withCount('foo'); - $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); } public function testWithCountAndSelect() @@ -784,7 +784,7 @@ public function testWithCountAndSelect() $builder = $model->select('id')->withCount('foo'); - $this->assertSame('select "id", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "id", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); } public function testWithCountAndMergedWheres() @@ -795,7 +795,7 @@ public function testWithCountAndMergedWheres() $q->where('bam', '>', 'qux'); }]); - $this->assertSame('select "id", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" and "bam" > ? and "active" = ?) as "active_foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "id", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" and "bam" > ? and "active" = ? limit 1) as "active_foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); $this->assertEquals(['qux', true], $builder->getBindings()); } @@ -813,7 +813,7 @@ public function testWithCountAndGlobalScope() // }); - $this->assertSame('select "id", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "id", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); } public function testWithCountAndConstraintsAndHaving() @@ -825,7 +825,7 @@ public function testWithCountAndConstraintsAndHaving() $q->where('bam', '>', 'qux'); }])->having('foo_count', '>=', 1); - $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" and "bam" > ?) as "foo_count" from "eloquent_builder_test_model_parent_stubs" where "bar" = ? having "foo_count" >= ?', $builder->toSql()); + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" and "bam" > ? limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs" where "bar" = ? having "foo_count" >= ?', $builder->toSql()); $this->assertEquals(['qux', 'baz', 1], $builder->getBindings()); } @@ -835,7 +835,7 @@ public function testWithCountAndRename() $builder = $model->withCount('foo as foo_bar'); - $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_bar" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_bar" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); } public function testWithCountMultipleAndPartialRename() @@ -844,7 +844,7 @@ public function testWithCountMultipleAndPartialRename() $builder = $model->withCount(['foo as foo_bar', 'foo']); - $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_bar", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_bar", (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); } public function testHasWithConstraintsAndHavingInSubquery() @@ -912,7 +912,7 @@ public function testWithCountAndConstraintsWithBindingInSelectSub() $q->selectSub($model->newQuery()->where('bam', '=', 3)->selectRaw('count(0)'), 'bam_3_count'); }]); - $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id") as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); + $this->assertSame('select "eloquent_builder_test_model_parent_stubs".*, (select count(*) from "eloquent_builder_test_model_close_related_stubs" where "eloquent_builder_test_model_parent_stubs"."foo_id" = "eloquent_builder_test_model_close_related_stubs"."id" limit 1) as "foo_count" from "eloquent_builder_test_model_parent_stubs"', $builder->toSql()); $this->assertSame([], $builder->getBindings()); } diff --git a/tests/Integration/Database/EloquentWithCountTest.php b/tests/Integration/Database/EloquentWithCountTest.php index cd64b1c4f030..e356f41e9d32 100644 --- a/tests/Integration/Database/EloquentWithCountTest.php +++ b/tests/Integration/Database/EloquentWithCountTest.php @@ -72,7 +72,7 @@ public function testSortingScopes() $result = Model1::withCount('twos')->toSql(); - $this->assertSame('select "one".*, (select count(*) from "two" where "one"."id" = "two"."one_id") as "twos_count" from "one"', $result); + $this->assertSame('select "one".*, (select count(*) from "two" where "one"."id" = "two"."one_id" limit 1) as "twos_count" from "one"', $result); } }