spatie / laravel-query-builder

Easily build Eloquent queries from API requests

Home Page:https://spatie.be/docs/laravel-query-builder

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to split out applied filters in generated query

wonder95 opened this issue · comments

Per the docs, I have created a custom filter like so:

<?php

namespace App\Custom\Filters;

use Illuminate\Database\Eloquent\Builder;
use Spatie\QueryBuilder\Filters\Filter;

class NameMemberNumberFilter implements Filter
{

    /**
     * @inheritDoc
     */
    public function __invoke(Builder $query, $value, string $property)
    {
        $query->where('last_name', 'like', "%{$value}%")
            ->orWhere('first_name', 'like', "%{$value}%")
            ->orWhere('member_number', 'like', "%{$value}%");
    }
}

and implemented it in my controller like so

    public function index()
    {
        return Inertia::render('Users/Index', [
            'users' => QueryBuilder::for(User::class)
                ->defaultSort('member_number')
                ->allowedSorts(['last_name', 'member_number'])
                ->allowedFilters(AllowedFilter::custom('name_number', new NameMemberNumberFilter), 'status', 'member_type')
                ->paginate(20)
                ->withQueryString()
        ]);
    }

By itself, it works fine. However, when I attempt to combine it with another filter, (e.g. 'status' or 'member_type'), the custom filter is applied, but the others are completely ignored.

http://members.test/users?filter[name_number]=Smith&filter[status]=Active

In this example, all the Smiths are returned, but not only the ones with a status of Active.

If I use status and member_type together, they work fine. The issue seems to be that when using a custom filter, it cannot be used with another filter. Have I somehow written or implemented my custom filter incorrectly? Or is this indeed a bug?

Turns out this is not a bug, but an issue with the query for my status and member_type fields. Per the docs

By default, any string values passed to allowedFilters() will automatically be converted to AllowedFilter::partial() filters.

so my query for the other fields was using LIKE, e.g.

SELECT count(*) as aggregate FROM `users` WHERE (`last_name` like '%Smith%' or `first_name` like '%Smith%' or `member_number` like '%Smith%' and LOWER(`users`.`status`) LIKE '%active%') and `users`.`deleted_at` IS NULL

In this case, because I also have a status of Inactive, so the LIKE query would get both. The logical action would be to use AllowedFilter::exact() like so

        return Inertia::render('Users/Index', [
            'users' => QueryBuilder::for(User::class)
                ->defaultSort('member_number')
                ->allowedSorts(['last_name', 'member_number'])
                ->allowedFilters(AllowedFilter::custom('name_number', new NameMemberNumberFilter),
                    AllowedFilter::exact('status'),
                    AllowedFilter::exact('member_type'))
                ->paginate(20)
                ->withQueryString()
        ]);

However, this still does not solve the problem, because of the way the query is generated. What it does is

SELECT count(*) as aggregate FROM `users` WHERE (`last_name` like '%Smith%' or `first_name` like '%Smith%' or `member_number` like '%Smith%' and LOWER(`users`.`status`) LIKE '%active%') and `users`.`deleted_at` IS NULL

Because of the way it is grouped, I still get an Inactive record that I don't want. I need to change the query to split out the users.active field to get what I want:

SELECT * FROM `users` 
WHERE (`last_name` like '%Smith%' or `first_name` like '%Smith%' or `member_number` like '%Smith%') and `users`.`status` = 'Active' 
and `users`.`deleted_at` IS NULL 
ORDER BY `member_number` 
ASC LIMIT 20 offset 0;

Since I am just specifying the available filters inside allowedFilters(), is there a way I can specify that the filters should be separate AND phrases and not all grouped in one WHERE clause?

Dear contributor,

because this issue seems to be inactive for quite some time now, I've automatically closed it. If you feel this issue deserves some attention from my human colleagues feel free to reopen it.