I have the following type
type Post {
id: Int!
title: String!
description: String!
created_at: DateTime!
updated_at: DateTime!
categories: [Category!]!
comments_count: Int! # <- ths one
}
I want to add an extra field to the paginated model - comments_count. It should be Post::withCount('comments'). What is the best way to do this?
I guess maybe using the @field directive (link for the docs).
I think this part from the docs is the most relevant for you:
Be aware that resolvers are not limited to root fields. A resolver can be used for basic tasks such as transforming the value of scalar fields, e.g. reformat a date.
type User {
created_at: String!
@field(resolver: "App\\Http\\GraphQL\\Types\\UserType@created_at")
}
Maybe in your case it's something like:
type Post {
....
comments_count: Int! @field(resolver: "App\\Http\\GraphQL\\Types\\PostType@commentsCount")
}
@eaverdeja I forgot to point that type Post is used together with @paginate directive, so with this approach field resolver will be called on each post, instead of calling ->withCount(). Or I'm wrong? 馃
That makes sense and seems like a N+1 problem. I suggest using using laravel telescope (if you can) or laravel debugar and check the query logs to confirm.
If that's the case, what about using the builder arg for the @paginate directive? From the docs:
If simply querying Eloquent does not fit your use-case, you can specify a custom builder.
type Query {
posts: [Post!]! @paginate(builder: "App\\Blog@visiblePosts")
}
Your method receives the typical resolver arguments and has to return an instance of
Illuminate\Database\Query\Builder.
<?php
namespace App;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Query\Builder;
use GraphQL\Type\Definition\ResolveInfo;
class Blog
{
public function visiblePosts($root, array $args, $context, ResolveInfo $resolveInfo): Builder
{
return DB::table('posts')
->where('visible', true)
->where('posted_at', '>', $args['after']);
}
}
So in your case maybe something like:
```
type Query {
posts: [Post!]! @paginate(builder: "App\ModelsPost@postsWithCommentCount")
}
...
namespace App\GraphQL\Types;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Query\Builder;
use GraphQL\Type\Definition\ResolveInfo;
class Post
{
...
public function postsWithCommentCount($root, array $args, $context, ResolveInfo $resolveInfo): Builder
{
return DB::table('posts')->withCount('comments');
}
}
@eaverdeja Yep, current solution looks like this:
public function withCommentsCount($root, array $args, Context $context, ResolveInfo $resolveInfo): Builder
{
$fields = array_filter(
array_keys($resolveInfo->getFieldSelection(1)['data']),
function (string $field): bool {
return $field !== 'comments_count';
}
);
return DB::table($this->getTable())
->select($fields)
->addSelect(DB::raw('(select count(*) from `comments` where `post_id` = id) as `comments_count`'));
}
@ElForastero Nice :raised_hands: Would you mind posting a more complete view of the solution (schema mainly)? I'm not sure how you are using this, but I guess something more reliant on eloquent would be easier, right?
p.s, Maybe we should move this to slack
@eaverdeja actually, this is not a really good solution, because it breaks built-in relations.
The query like this wouldn't work, and you will have to manually resolve all relations.
type Post {
id: Int!
title: String!
description: String!
created_at: DateTime!
updated_at: DateTime!
categories: [Category!]! # <- this doesn't work anymore
comments_count: Int! # <- we have solved this problem
}
The main problem is that we don't have control on query builder. We can assemble the query from scratch using paginate's builder argument, but not extend it.
And about N+1 problem - it is present with default @paginate directive.

Yeah, we can definitely do better. I'll try to spin up an example of my own
Isn't there some hook or event raised right before a query get run by Lighthouse, which allows to customize the query and solve a problem like this?
@emielmolenaar Probably it would be nice, but as far as I get from reading the sources, there are only 2 events for the request, but nothing useful for models.
Paginate directive itself is pretty readable. It gets the model or the builder you have provided via argument, applies scopes and calls ->paginate(). No extra events or callbacks.
It seems to me the default directives should have some extension points so this kind of stuff could be configurable instead of us creating a custom directive or even a resolver. Am I thinking straight?
All I have in mind about this, is passing somehow instances of models or builder itself though directives. This way it will be possible and to add custom fields, and apply custom filters in custom directives.
For instance in my case I would create a withCount directive and use this way:
type User {
id: ID!
name: String!
phones_count: Int! @withCount('phones')
}
All I need is to call a method on eloquent model. Oversimplified version can looks like the example below.
public function handleField(FieldValue $value, ExecutionContext $context): FieldValue
{
$model = $context->getModel();
$relation = $this->directiveArgValue('relation');
return $value->setResolver(function () use ($model, $relation) {
return $model->withCount($relation);
});
}
Maybe @spawnia and @chrissm79 have some thoughs on this? 馃
Surely this approach has some problems.
Hey all, apologizes I've been slammed lately. What you're looking for here is a deferred field to get around the N+1 issue. Here's a rough example:
Custom Batch Loader
namespace App\GraphQL;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Execution\DataLoader\BatchLoader;
class WithCountLoader extends BatchLoader
{
/**
* @var Builder
*/
protected $builder;
/**
* @var string
*/
protected $relation;
/**
* @param Builder $builder
* @param string $relation
*/
public function __construct(Builder $builder, string $relation)
{
$this->builder = $builder;
$this->relation = $relation;
}
/**
* Resolve the keys.
*
* The result has to be a map: [key => result]
*/
public function resolve(): array
{
$parents = collect(Arr::pluck($this->keys, 'parent'));
return $this->builder->withCount($this->relation)
->findMany($parents->pluck('id'))
->mapWithKeys(function ($post) {
$property = "{$this->relation}_count";
return [$post->getKey() => $post->{$property}];
})
->all();
}
}
Field Resolver
namespace App\GraphQL\Types;
use App\GraphQL\WithCountLoader;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Execution\DataLoader\BatchLoader;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class PostType
{
/**
* Resolve comment count.
*
* @param Post $post
* @param array $args
* @param GraphQLContext $context
* @param ResolveInfo $info
*
* @return \GraphQL\Deferred
*/
public function commentsCount(Post $post, array $args, GraphQLContext $context, ResolveInfo $info)
{
$dataloader = BatchLoader::instance(
WithCountLoader::class,
$info->path,
[
'builder' => \App\Models\Post::query(),
'relation' => 'events',
]
);
return $dataloader->load(
$post->getKey(),
['parent' => $post]
);
}
}
Then in your graphql file:
type Post {
id: Int!
title: String!
description: String!
created_at: DateTime!
updated_at: DateTime!
categories: [Category!]!
comments_count: Int! @field(resolver: "App\\GraphQL\\Types\\PostType@commentsCount")
}
@chrissm79 That's really cool! I still feel these features and recipes from lighthouse should have better docs. This is solution is not complicated, but arriving at it alone just from reading the docs and using lighthouse isn't so easy. I know this is opensource and I would love to help with that :)
@eaverdeja If you would like to add an example to the docs I'd be more than happy to accept it! The "Guides" section would be a great place for this. If you have any questions just let me know!
hey guys I just started using this package and I love it. I came here because I was looking for a @withCount directive and couldn't find it. It would be awesome to have it. Awesome job with this package by the way! It seems to be more complete and easier to get started with than the other ones I stumbled upon around here.
Most helpful comment
Hey all, apologizes I've been slammed lately. What you're looking for here is a deferred field to get around the N+1 issue. Here's a rough example:
Custom Batch Loader
Field Resolver
Then in your graphql file: