Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Refines Queries from Request #49611

Closed
wants to merge 2 commits into from
Closed

[10.x] Refines Queries from Request #49611

wants to merge 2 commits into from

Conversation

DarkGhostHunter
Copy link
Contributor

@DarkGhostHunter DarkGhostHunter commented Jan 8, 2024

What?

This is a relatively big-but-small PR that adds the ability to "refine" a query. This allows to refine a database query based on the request query by moving that logic away from the controller. This PR adds the illuminate/refine package for the main reason it depends on both illuminate/database and illuminate/http.

For example, let's imagine we want to filter the posts safely from the database from this request:

GET https://mynewssite.com/posts?author_id=1&order_by=published_at

The normal way to filter that would be to push that logic into the controller itself:

use Illuminate\Http\Request;
use App\Models\Post;

public function (Request $request)
{
    $request->validate([
        // ... 
    ])

    return Post::when($request->has('author_id'))->where('author_id', $request->get('author_id'))
               ->when($request->has('order_by'))->orderBy($request->get('order_by'))
               ->when($request->has('order_by_desc'))->orderByDesc($request->get('order_by_desc'))
               ->paginate();
}

The controller will grow in size at the same time more query keys are expected to manage the query, especially when managing virtual states of models (that are equivalent to multiple WHERE clauses on multiple columns), and adding gates to check if the user can use a given key.

Instead, we could move all that logic into what I define as a "Refiner". This class is relatively simple to understand: for each key present in the Request, call the matching method (in camelCase) on the Refiner class.

use Illuminate\Http\Request;
use App\Models\Post;
use App\Http\Refiners\PostRefiner;

public function (Request $request)
{
    $request->validate([
        // ... ALWAYS VALIDATE YOUR INPUT OR QUERY
    ])

    return Post::refineBy(PostRefiner::class)->paginate();
}

All the logic is moved down to the "Refiner" class. That class is resolved by the container. That class is managed opaquely by another class (the "Query Refiner") that injects the current Request and the calls over that aforementioned Refiner matching methods.

That Refiner looks like this:

namespace App\Http\Refiners;

use Illuminate\Refine\Refiner;

class PostRefiner extends Refiner
{
    public function authorId($query, $value)
    {
        $query->where('author_id', $value)
    }
    
    public function orderBy($query, $value)
    {
        $query->orderBy($value);
    }
    
    public function orderByDesc($query, $value)
    {
        $query->orderByDesc($value);
    }
}

Not only we moved the refinement outside controller, it also moves the condition away from the actual refinement logic.

The Refiner also allows to execute something on the Builder and Request before and after with the before() and after() method, respectively.

namespace App\Http\Refiners;

use Illuminate\Refine\Refiner;

class PostRefiner extends Refiner
{
    public function before($builder, $request)
    {
        //...
    }
    
    public function after($builder, $request)
    {
        //...
    }
}

Finally, not every request will require all the keys. The developer can choose to limit the keys to use from the request using the keys() method. By default, it always returns all the keys from the Request, but in this following example it only checks for two keys.

namespace App\Http\Refiners;

use Illuminate\Http\Request;
use Illuminate\Refine\Refiner;

class PostRefiner extends Refiner
{
    public function keys(Request $request): array
    {
        return ['author_id', 'order_by']
    }
}

This PR also includes tests for the Refine, the make:refiner command (which creates a file on app\Http\Refiners) and its test.

@driesvints
Copy link
Member

Heya. I really don't think this should be a new component. I feel this is a good 3rd party "package" candidate but not something that should go into core.

@dammy001
Copy link
Contributor

dammy001 commented Jan 8, 2024

Heya. I really don't think this should be a new component. I feel this is a good 3rd party "package" candidate but not something that should go into core.

I agree with @driesvints

@DarkGhostHunter
Copy link
Contributor Author

Got it guys. I thought everyone had the same problem and that could be fixed ootb.

@DarkGhostHunter DarkGhostHunter deleted the feat/refine-query-by-request branch January 8, 2024 14:03
@driesvints
Copy link
Member

No worries @DarkGhostHunter. Appreciate you trying to contribute here 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants