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

bishtyogeshsingh's avatar

Centralizing Query Logic in Laravel: Best Practices for Reusable Functions

I’m developing a website where I need to fetch a list of users on some pages, but I only need limited data about the users, not all their details. Currently, I can handle this using models and write queries in each controller. However, I’m searching for a simpler approach where I can centralize the query logic in a single place (a "middle layer").

For example, if I need user data, I should be able to call a reusable function and retrieve it instead of writing the same query repeatedly across controllers. This approach should not only apply to user details but also other conditional or complex Laravel database queries that are being duplicated in multiple places.

Is it better to define such reusable functions directly in the model, or is there a middle-layer approach in Laravel that is more suitable for this? Suggestions are welcome.

2 likes
3 replies
eszterczotter's avatar

I think what you are describing here is the Repository pattern. Or at least that is what many of us would call the "middle layer" that you are asking for, which would be responsible for fetching the data. This pattern has been covered here at Laracasts all over the place, but you can also look it up in general on the internet. It is an implementation of the Strategy Pattern, which is covered in this video: https://laracasts.com/series/design-patterns-in-php/episodes/4 And the reason for its existence is different than the problem you're trying to solve. It has been born to make it simpler to switch DB engines, which is not really necessary with something like Eloquent already at our fingertips.

So I don't know if I would ever reach for it in a Laravel app now. Laravel provides other options, like Query Scopes: https://laravel.com/docs/11.x/eloquent#query-scopes These can be organized neatly into classes and traits and then you have them available in any controller.

Another option would be to just use an Action for each query you would want to reuse. Just create a class named however you like, for example: GetUserDetailsand create a method on it, again called whatever you want, for example: execute()and then you can use it in a controller like this:

public function index(GetUserDetails $query)
{
		$userDetails = $query->execute();
		// ...
}

And you can reuse it anywhere you have dependency injection, furthermore, you have dependency injection available in the constructor of GetUserDetails as well, should you need it. Which means you can test the action itself with unit tests. And also, if you want or need, you can use other actions/queries in it. So no need to create a single "repository" class in order to organize the code into smaller pieces.

And another option is to use an external package like this one from Spatie which has its own rules: https://spatie.be/docs/laravel-query-builder/v5/introduction It constrains you more but can save you some boilerplate, if and only if your usecase needs that. It really depends on what your queries are for. If it's for filtering, it might help you; if not, it might not help you.

And obviously you can always combine all of these options. You can use the package for its use cases, you can have Action/Query classes and for the most reused queries, you can have local or global query scopes as well.

1 like
martinbean's avatar

@bishtyogeshsingh Either use local scopes to encapsulate the query clauses, or—my least favourite option—create a repository class that has methods to return entities matching specific criteria:

class UserRepository
{
    public function verified()
    {
        return User::query()->whereNotNull('email_verified_at')->get();
    }
}

My biggest issue with repositories is, as you need to support more and more use cases, you just end up having to create more and more methods in your repository. For example, the above method would fetch all verified users. You then probably want a method to paginate the results:

class UserRepository
{
    public function verified()
    {
        return User::query()->whereNotNull('email_verified_at')->get();
    }

+   public function verifiedPaginated()
+   {
+       return User::query()->whereNotNull('email_verified_at')->paginate();
+   }
}

But then you get a requirement where you want to see verified users, sorted by the date they verified their email address, so have to create another method:

class UserRepository
{
    public function verified()
    {
        return User::query()->whereNotNull('email_verified_at')->get();
    }

    public function verifiedPaginated()
    {
        return User::query()->whereNotNull('email_verified_at')->paginate();
    }

+   public function verifiedPaginatedSortedByEmailVerifiedAt()
+   {
+       return User::query()->whereNotNull('email_verified_at')->orderBy('email_verified_at', 'desc')->paginate();
+   }
}

You just end up adding more methods to try and accommodate all of these business rules that only differ slightly, all whilst losing all of the benefits and features an ORM gives you in the first place.

bishtyogeshsingh's avatar

@martinbean Yes, you're right. So, should I use Laravel's default ORM feature for the same query in multiple controllers?

Please or to participate in this conversation.