Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

DimZ's avatar
Level 4

Where to store query builder methods?

Hey guys,

My app have Users and Badges. A Badge is simply a reward when you achieve some actions into the app.

I have a Controller to show all badges, with pivot data about user progression/achievements. We can also filter this list and, for doing that, I'm using laravel-query-builder package from spatie.

Here is my code for now:

class Badge extends Model {
    public function users()
    {
        return $this->belongsToMany(User::class, BadgeProgression::class)
            ->withPivot('value', 'achieved_at')
            ->as('progression')
            ->withTimestamps();
    }

    public function progression()
    {
        return $this->hasMany(BadgeProgression::class);
    }
}
class User extends Authenticatable {
    public function badges()
    {
        return $this->belongsToMany(Badge::class, BadgeProgression::class)
            ->withPivot('value', 'achieved_at')
            ->as('progression')
            ->withTimestamps();
    }
}
class BadgeProgression extends Pivot
{
    protected $table = "badge_user_progression";

    public function getIsAchievedAttribute(): bool
    {
        return (bool) $this->achieved_at;
    }
}
class ListUserBadgesQuery extends QueryBuilder
{
    public function __construct(User $user, PaginateRequest $request)
    {
        $query = Badge::with([
            'media',
            'progression' => fn ($q) => $q->where('user_uuid', $user->uuid)
        ]);

        parent::__construct($query, $request);

        $this
            ->allowedFilters(
                'uuid',
                'name',
                'description',
            )
            ->allowedSorts(
                'uuid',
                'name',
                'created_at',
                'updated_at',
            )
            ->allowedFields([
                'uuid',
                'name',
                'description',
                'created_at',
                'updated_at',
            ]);
    }
}
class ListUserBadgesController extends Controller
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(
        User $user,
        PaginateRequest $request,
    ) {
        $query = new ListUserBadgesQuery($user, $request);
        
        $badges = $query
            ->paginate(
                $request->input('per_page')
            );

        return BadgeResource::collection($badges);
    }
}

As you can see, I can have user progression details here: 'progression' => fn ($q) => $q->where('user_uuid', $user->uuid). But I need to do this in different places, and also this kind of stuff for lots of other Models.

So my question is, what is the best way to manage stuff like this, and avoid repeating myself every time?

I think Scopes is the pretty good place, but I'm feeling I will have a lot of scopes methods. I also think about Repository, but because I'm using laravel-query-builder package, I'd like to get the query builder instead of the list of results, not filtered yet.

in short, your ideas are welcome!

0 likes
1 reply
DimZ's avatar
Level 4

This is my current update:

class Badge extends Model {
    public function users()
    {
        return $this->belongsToMany(User::class, BadgeProgression::class)
            ->withPivot('value', 'achieved_at')
            ->as('progression')
            ->withTimestamps();
    }

    public function progression()
    {
        return $this->hasMany(BadgeProgression::class);
    }
}
class ListUserBadgesQuery extends QueryBuilder
{
    public function __construct(User $user, PaginateRequest $request)
    {
        $query = Badge::with([
            'media',
        ])
            ->withProgressionOfUser($user);

        parent::__construct($query, $request);

        // ...  
}

This is working fine, but I don't feel it is the right way to do it, because progression relationship can be misunderstood. If later I do Badges::with(['progression'])->get() I will have only one single progression data of a random user. So the results could be unexpected.

As usual, your ideas are welcome 😄

Please or to participate in this conversation.