protonemedia / inertiajs-tables-laravel-query-builder

Inertia.js Tables for Laravel Query Builder
https://protone.media/en/blog/introducing-inertiajs-tables-a-datatables-like-package-for-laravel-query-builder
MIT License
435 stars 123 forks source link

Ideas for a better global search filter implementation #94

Open darkons opened 1 year ago

darkons commented 1 year ago

Hello,

I currently use global search in almost all my tables. I was looking for a way to improve the example documentation and would like to collaborate with other users on a possible cleaner option.

Currently according to the documentation you can do this:

$globalSearch = AllowedFilter::callback('global', function ($query, $value) {
    $query->where(function ($query) use ($value) {
        Collection::wrap($value)->each(function ($value) use ($query) {
            $query
                ->orWhere('name', 'LIKE', "%{$value}%")
                ->orWhere('email', 'LIKE', "%{$value}%");
        });
    });
});

And use like:

$users = QueryBuilder::for(User::class)
    ->defaultSort('name')
    ->allowedSorts(['name', 'email', 'language_code'])
    ->allowedFilters(['name', 'email', 'language_code', $globalSearch])
    ->paginate()
    ->withQueryString();

This works fine, but does not allow you to search through relationships globally in a simple and clean way.

Instead of the above, you can create a custom filter class like this:

<?php

namespace App\Classes\QueryBuilder\Filters;

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

class GlobalSearchFilter implements Filter
{
    public function __construct(private array $keys)
    {
    }

    public function __invoke(Builder $query, $value, string $property): void
    {
        $query->where(function ($query) use ($value) {
            Collection::wrap($value)->each(function ($value) use ($query) {
                Collection::make($this->keys)->each(function ($key) use ($value, $query) {
                    if (str_contains($key, '.')) {
                        list($column, $relation) = $this->parseRelationKey($key);

                        $constraint = fn ($query) => $query->where($column, 'LIKE', "%{$value}%");

                        return $query->orWhereHas($relation, $constraint)
                            ->with($relation, $constraint);
                    }

                    return $query->orWhere($key, 'LIKE', "%{$value}%");
                });
            });
        });
    }

    private function parseRelationKey(string $key)
    {
        $array = explode('.', $key);
        $column = array_pop($array);
        $relation = implode('.', $array);

        return [$column, $relation];
    }
}

Then you can search through model columns and relationships with a few lines of code keeping your controller more cleaner:

$users = QueryBuilder::for(User::class)
    ->defaultSort('name')
    ->allowedSorts(['name', 'email', 'language_code'])
    ->allowedFilters(['name', 'email', 'language_code', AllowedFilter::custom('global', new GlobalSearchFilter([
        'name',
        'phone.number',
        'another.nested.relation',
    ])])
    ->paginate()
    ->withQueryString();

I am not a query builder expert so the filter could very possibly be optimised. Any suggestions would be appreciated.

I just wanted to share this idea with other users to improve the implementation of this table feature. Maybe we could get an almost perfect global filter class that could be included in the package itself.