>  기사  >  PHP 프레임워크  >  Laravel 쿼리 필터를 구현하는 방법을 단계별로 알려드립니다.

Laravel 쿼리 필터를 구현하는 방법을 단계별로 알려드립니다.

青灯夜游
青灯夜游앞으로
2023-01-18 21:10:491358검색

Laravel 쿼리 필터를 구현하는 방법을 단계별로 알려드립니다.

쿼리 필터... 시스템 개발 시 자주 묻는 질문입니다. 그러나 코드 작성을 시작할 때 모든 개발자에게 발생하는 친숙한 질문이 많이 있습니다. "이 쿼리 로직을 어디에 두어야 할까요? 사용하기 쉽도록 어떻게 관리해야 할까요?" 솔직히 저는 제가 개발하는 모든 프로젝트에 대해 제가 만든 이전 프로젝트에서의 경험을 바탕으로 다른 스타일로 글을 씁니다. 그리고 새 프로젝트를 시작할 때마다 이번에는 쿼리 필터를 어떻게 정렬해야 하는지 스스로에게 묻곤 합니다. 이 기사는 해당 문제가 있는 쿼리 필터링 시스템의 단계별 개발로 간주될 수 있습니다.

Context

이 글을 쓰는 시점에는 PHP 8.1과 MySQL 8에서 Laravel 9를 사용하고 있습니다. 기술 스택은 큰 문제가 아니라고 생각합니다. 여기서는 주로 쿼리 필터 시스템 구축에 중점을 둡니다. 이 기사에서는 사용자 테이블에 대한 필터를 작성하는 방법을 설명합니다.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * 运行迁移
     *
     * @return void
     */
    public function up()
    {
        Schema::create(&#39;users&#39;, function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('gender', 10)->nullable()->index();
            $table->boolean('is_active')->default(true)->index();
            $table->boolean('is_admin')->default(false)->index();
            $table->timestamp('birthday')->nullable();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * 回退迁移
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

또한 쿼리를 쉽게 모니터링하기 위해 Laravel Telescope를 사용합니다.

Initial Point

Laravel 사용법을 처음 배운 날에는 컨트롤러에서 직접 필터를 호출하는 경우가 많았습니다. 간단하고 마법이 없으며 이해하기 쉽지만 이 방법에는 문제가 있습니다.

  • 컨트롤러에 많은 로직이 배치되어 컨트롤러가 부풀어 오릅니다.
  • 재사용할 수 없습니다.
  • 동일한 작업이 많이 반복됩니다.
<?php

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    public function __invoke(Request $request)
    {
        // /users?name=ryder&email=hartman&gender=male&is_active=1&is_admin=0&birthday=2014-11-30
        $query = User::query();

        if ($request->has('name')) {
            $query->where('name', 'like', "%{$request->input('name')}%");
        }

        if ($request->has('email')) {
            $query->where('email', 'like', "%{$request->input('email')}%");
        }

        if ($request->has('gender')) {
            $query->where('gender', $request->input('gender'));
        }

        if ($request->has('is_active')) {
            $query->where('is_active', $request->input('is_active') ? 1 : 0);
        }

        if ($request->has('is_admin')) {
            $query->where('is_admin', $request->input('is_admin') ? 1 : 0);
        }

        if ($request->has('birthday')) {
            $query->whereDate('birthday', $request->input('birthday'));
        }

        return $query->paginate();

        // select * from `users` where `name` like '%ryder%' and `email` like '%hartman%' and `gender` = 'male' and `is_active` = 1 and `is_admin` = 0 and date(`birthday`) = '2014-11-30' limit 15 offset 0
    }
}

Use Local Scope

필터링 중에 로직을 숨길 수 있도록 Laravel의 Local Scope를 사용해 보겠습니다. 쿼리를 User 모델의 함수 범위로 변환합니다:

// User.php
public function scopeName(Builder $query): Builder
{
    if (request()->has('name')) {
        $query->where('name', 'like', "%" . request()->input('name') . "%");
    }
    return $query;
}

public function scopeEmail(Builder $query): Builder
{
    if (request()->has('email')) {
        $query->where('email', 'like', "%" . request()->input('email') . "%");
    }
    return $query;
}

public function scopeGender(Builder $query): Builder
{
    if (request()->has('gender')) {
        $query->where('gender', request()->input('gender'));
    }
    return $query;
}

public function scopeIsActive(Builder $query): Builder
{
    if (request()->has('is_active')) {
        $query->where('is_active', request()->input('is_active') ? 1 : 0);
    }
    return $query;
}

public function scopeIsAdmin(Builder $query): Builder
{
    if (request()->has('is_admin')) {
        $query->where('is_admin', request()->input('is_admin') ? 1 : 0);
    }
    return $query;
}

public function scopeBirthday(Builder $query): Builder
{
    if (request()->has('birthday')) {
        $query->where('birthday', request()->input('birthday'));
    }
    return $query;
}

// UserController.php
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

     $query = User::query()
             ->name()
            ->email()
            ->gender()
            ->isActive()
            ->isAdmin()
            ->birthday();

    return $query->paginate();

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}

이 설정을 통해 대부분의 데이터베이스 작업을 모델 클래스로 옮겼지만 코드 중복이 많았습니다. 예시 2에는 동일한 이름과 이메일 범위 필터, 동일한 성별 생일 및 is_active/is_admin 그룹이 있습니다. 유사한 쿼리 기능을 그룹화하겠습니다.

// User.php
public function scopeRelativeFilter(Builder $query, $inputName): Builder
{
    if (request()->has($inputName)) {
        $query->where($inputName, 'like', "%" . request()->input($inputName) . "%");
    }
    return $query;
}

public function scopeExactFilter(Builder $query, $inputName): Builder
{
    if (request()->has($inputName)) {
        $query->where($inputName, request()->input($inputName));
    }
    return $query;
}

public function scopeBooleanFilter(Builder $query, $inputName): Builder
{
    if (request()->has($inputName)) {
        $query->where($inputName, request()->input($inputName) ? 1 : 0);
    }
    return $query;
}

// UserController.php
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    $query = User::query()
        ->relativeFilter('name')
        ->relativeFilter('email')
        ->exactFilter('gender')
        ->booleanFilter('is_active')
        ->booleanFilter('is_admin')
        ->exactFilter('birthday');

    return $query->paginate();

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}

이 시점에서 우리는 대부분의 중복 항목을 그룹화했습니다. 그러나 if 문을 제거하거나 이러한 필터를 다른 모델로 확장하는 것은 약간 어렵습니다. 우리는 이 문제를 완전히 해결할 수 있는 방법을 찾고 있습니다.

파이프라인 디자인 패턴 사용

파이프라인 디자인 패턴 은 일련의 작업을 단계별로 구축하고 수행할 수 있는 기능을 제공하는 디자인 패턴입니다. Laravel에는 실제로 이 디자인 패턴을 쉽게 적용할 수 있게 해주는 파이프라인이 내장되어 있지만 어떤 이유로 공식 문서에는 나열되어 있지 않습니다. Laravel 자체도 요청과 응답 사이의 미들웨어로 파이프라인을 사용합니다. 가장 기본적으로 Laravel에서 Pipeline을 사용하려면

app(\Illuminate\Pipeline\Pipeline::class)
    ->send($intialData)
    ->through($pipes)
    ->thenReturn(); // data with pipes applied

를 사용할 수 있습니다. 문제의 경우 필터 단계를 통해 초기 쿼리 User:query()를 파이프라인에 전달하고 필터를 적용하는 쿼리 빌더를 반환할 수 있습니다. .

// UserController
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    $query = app(Pipeline::class)
        ->send(User::query())
        ->through([
            // filters
        ])
        ->thenReturn();

    return $query->paginate();

이제 파이프라인 필터를 구축해야 합니다.

// File: app/Models/Pipes/RelativeFilter.php

<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

class RelativeFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query->where($this->inputName, 'like', "%" . request()->input($this->inputName) . "%");
        }
        return $next($query);
    }
}

// File: app/Models/Pipes/ExactFilter.php

<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

class ExactFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query->where($this->inputName, request()->input($this->inputName));
        }
        return $next($query);
    }
}

//File: app/Models/Pipes/BooleanFilter.php
<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

class BooleanFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query->where($this->inputName, request()->input($this->inputName) ? 1 : 0);
        }
        return $next($query);
    }
}

// UserController
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    $query = app(Pipeline::class)
        ->send(User::query())
        ->through([
            new \App\Models\Pipes\RelativeFilter('name'),
            new \App\Models\Pipes\RelativeFilter('email'),
            new \App\Models\Pipes\ExactFilter('gender'),
            new \App\Models\Pipes\BooleanFilter('is_active'),
            new \App\Models\Pipes\BooleanFilter('is_admin'),
            new \App\Models\Pipes\ExactFilter('birthday'),
        ])
        ->thenReturn();

    return $query->paginate();

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}

각 쿼리 논리를 별도의 클래스로 이동하여 다형성, 상속, 캡슐화, 추상화를 포함하여 OOP를 사용하여 사용자 정의 가능성을 잠금 해제합니다. 예를 들어, 파이프라인의 핸들 함수에서 if 문의 논리만 다르다는 것을 볼 수 있습니다. 추상 클래스 BaseFilter

//File: app/Models/Pipes/BaseFilter.php

<?php

namespace App\Models\Pipes;

use Illuminate\Database\Eloquent\Builder;

abstract class BaseFilter
{
    public function __construct(protected string $inputName)
    {
    }

    public function handle(Builder $query, \Closure $next)
    {
        if (request()->has($this->inputName)) {
            $query = $this->apply($query);
        }
        return $next($query);
    }

    abstract protected function apply(Builder $query): Builder;
}

// BooleanFilter
class BooleanFilter extends BaseFilter
{
    protected function apply(Builder $query): Builder
    {
        return $query->where($this->inputName, request()->input($this->inputName) ? 1 : 0);
    }
}

// ExactFilter
class ExactFilter extends BaseFilter
{
    protected function apply(Builder $query): Builder
    {
        return $query->where($this->inputName, request()->input($this->inputName));
    }
}

// RelativeFilter
class RelativeFilter extends BaseFilter
{
    protected function apply(Builder $query): Builder
    {
        return $query->where($this->inputName, 'like', "%" . request()->input($this->inputName) . "%");
    }
}

를 생성하여 분리하고 추상화하겠습니다. 이제 필터는 직관적이고 재사용성이 뛰어나며 쉽게 사용할 수 있습니다. 구현하고 확장하려면 파이프라인을 생성하고 BaseFilter를 확장한 다음 Apply 함수를 선언하여 파이프라인에 적용하면 됩니다.

로컬 범위와 파이프라인 결합

이 시점에서는 파이프라인을 호출하는 모델에 범위를 생성하여 코드를 더 깔끔하게 만들기 위해 컨트롤러에서 파이프라인을 숨기려고 합니다.

// User.php
public function scopeFilter(Builder $query)
{
    $criteria = $this->filterCriteria();
    return app(\Illuminate\Pipeline\Pipeline::class)
        ->send($query)
        ->through($criteria)
        ->thenReturn();
}

public function filterCriteria(): array
{
    return [
        new \App\Models\Pipes\RelativeFilter('name'),
        new \App\Models\Pipes\RelativeFilter('email'),
        new \App\Models\Pipes\ExactFilter('gender'),
        new \App\Models\Pipes\BooleanFilter('is_active'),
        new \App\Models\Pipes\BooleanFilter('is_admin'),
        new \App\Models\Pipes\ExactFilter('birthday'),
    ];
}

// UserController.php
public function __invoke(Request $request)
{
    // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11

    return User::query()
        ->filter()
        ->paginate()
        ->appends($request->query()); // 将所有当前查询附加到分页链接中

    // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0
}

이제 사용자는 어디에서나 필터를 호출할 수 있습니다. 그러나 다른 모델도 필터링을 구현하기를 원하므로 범위를 포함하는 특성을 생성하고 모델 내부의 필터링 프로세스에 참여하는 파이프라인을 선언합니다.

// User.php

use App\Models\Concerns\Filterable;

class User extends Authenticatable {
        use Filterable;

        protected function getFilters()
        {
            return [
                new \App\Models\Pipes\RelativeFilter('name'),
                new \App\Models\Pipes\RelativeFilter('email'),
                new \App\Models\Pipes\ExactFilter('gender'),
                new \App\Models\Pipes\BooleanFilter('is_active'),
                new \App\Models\Pipes\BooleanFilter('is_admin'),
                new \App\Models\Pipes\ExactFilter('birthday'),
            ];
        }

        // 其余代码

// File: app/Models/Concerns/Filterable.php

namespace App\Models\Concerns;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pipeline\Pipeline;

trait Filterable
{
    public function scopeFilter(Builder $query)
    {
        $criteria = $this->filterCriteria();
        return app(Pipeline::class)
            ->send($query)
            ->through($criteria)
            ->thenReturn();
    }

    public function filterCriteria(): array
    {
        if (method_exists($this, 'getFilters')) {
            return $this->getFilters();
        }

        return [];
    }
}

우리는 분할 정복 문제를 해결했습니다. 이제 모든 파일, 모든 클래스, 모든 기능에 명확한 책임이 있습니다. 코드도 깔끔하고 직관적이며 재사용하기 쉽습니다. 이 포스트 Demo의 전체 과정에 대한 코드를 여기에 넣었습니다.

결론

위는 고급 쿼리 필터 시스템을 구축하는 방법의 일부이며 로컬 범위 및 특히 파이프라인 디자인 패턴과 같은 일부 Laravel 프로그래밍 방법을 소개합니다. 이 설정을 새 프로젝트에 빠르고 쉽게 적용하려면 미리 빌드된 파이프라인 세트가 포함된 파이프라인 쿼리 컬렉션 패키지를 사용하면 쉽게 설치하고 사용할 수 있습니다. 여러분 모두 저를 지지해주셨으면 좋겠습니다!

원본 주소: https://baro.rezonia.com/blog/building-a-sexy-query-filter

번역 주소: https://learnku.com/laravel/t/68762

더 보기 프로그래밍 관련 지식은 프로그래밍 비디오를 방문하세요! !

위 내용은 Laravel 쿼리 필터를 구현하는 방법을 단계별로 알려드립니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 learnku.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제