Условные фильтры в Laravel

Условные фильтры в Laravel

Вам когда-нибудь приходилось применять фильтры к запросу, только когда пользователь действительно применяет их? Метод Laravel query builder when() специально для вас.

Недавно я столкнулся с этой же проблемой, работая над проектом клиента. Я должен был отобразить список потенциальных клиентов и позволить пользователю условно применить один или несколько доступных фильтров, таких как имя клиента, статус лидов и т. д.

Конечно, это не сложная и не редкая проблема. На самом деле, я должен был реализовать его так или иначе почти в каждом проекте, над которым я работал в своей карьере разработчика. Единственная проблема здесь заключается в том, что чаще всего вы пишете оператор if для каждого из фильтров, чтобы проверить, следует ли его применять к запросу, что, честно говоря, делает код очень уродливым.

Подготовка

Возьмем следующий пример. У нас есть две модели: Post и Comment, связанные отношением “один ко многим” (сообщение имеет много комментариев, комментарий принадлежит сообщению). Например, я старался сделать все как можно проще – вместо того чтобы создавать другую Topic модель для наших тем постов, я использовал строки. В любом случае, вот наши модели и миграции:

// App/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    use HasFactory;

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}
// App/Models/Comment.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    use HasFactory;

    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

// database/migrations/2020_10_22_180415_create_posts_table.php

<?php

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

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('topic');
            $table->string('username');
            $table->string('title');
            $table->text('content');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
}
// database/migrations/2020_10_22_180506_create_comments_table.php

<?php

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

class CreateCommentsTable extends Migration
{
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->foreignId('post_id')->constrained()->cascadeOnDelete();
            $table->string('username');
            $table->text('content');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

Теперь, ради нашего примера, я создал страницу, которая отображает только список сообщений, каждый блок сообщений содержит свою тему, имя пользователя и количество комментариев. Вот как это выглядит:

Чтобы отобразить сообщения, я использовал следующий код в своем PostsController классе:

// App/Http/Controllers/PostsController.php

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::query()
		    ->latest()
            ->withCount('comments')
		    ->get();

        return view('posts.index')->with(['posts' => $posts]);
    }
}

Применение фильтров

Как вы можете видеть, в списке сообщений есть два фильтра: тема и популярность. Фильтр тем позволяет пользователю выбрать конкретную тему, в то время как фильтр популярности имеет два варианта: все и популярные, которые представляют собой сообщения, имеющие более 10 комментариев.

Мой первый шаг (который совершенно необязателен) к фильтрации нашего запроса состоял в том, чтобы извлечь запрос в отдельный вызываемый метод applyFilters. На мой взгляд, это просто делает код более читабельным.

Следующий (и последний) довольно прост. Нам нужно проверить, содержит ли строка запроса непустое значение нашего ключа фильтра (topicOfPopularity), и если да – применить его к фильтру. В приложении можно сделать что-то вроде этого:

// App/Http/Controllers/PostsController.php

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function index(Request $request)
    {
        $posts = $this->applyFilters($request);

        return view('posts.index')->with(['posts' => $posts]);
    }

    private function applyFilters(Request $request): Collection
    {
        $query = Post::query()
            ->withCount('comments')
            ->latest();

        if ($topic = $request->query('topic')) {
            $query->where('topic', $topic);
        }

        if ($request->query('popularity')) {
            $query->having('comments_count', '>', 10);
        }

        return $query->get();
    }
}

Что ж, это работает. Но что, если бы у нас было еще 10 фильтров? Я даже не хочу думать о том, как он будет выглядеть с десятками других фильтров. Меня беспокоит не то, что каждый фильтр имеет свою собственную ссылку в коде. Как видите, каждый фильтр может иметь несколько иную логику. Фильтр topic имеет простой where, в то время как фильтр popularity основан на значении, которое было запрошено с помощью подзапроса (это то, что withCount делает, чтобы помочь нам избежать проблемы n+1), поэтому мы должны использовать having вместо where. И конечно, ваше приложение может иметь фильтры, которые гораздо сложнее.

К счастью, конструктор запросов Laravel оснащен методом when, который позволяет нам применять предложения к нашим запросам. Из официальной документации Laravel:

Первый аргумент метода when() это значение нашего фильтра. Если он не пуст, то выполнится замыкание, которое передаем вторым аргументом. Замыкание принимает экземпляр Illuminate\Database\Eloquent\Builder и значение нашего фильтра. Замыкание должно вернуть Illuminate\Database\Eloquent\Builder.

Итак, после короткого рефакторинга, вот как выглядит наш PostsController:

// App/Http/Controllers/PostsController.php

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function index(Request $request)
    {
        $posts = $this->applyFilters($request);

        return view('posts.index')->with(['posts' => $posts]);
    }

    private function applyFilters(Request $request): Collection
    {
        return Post::query()
            ->withCount('comments')
            ->latest()
            ->when($request->query('topic'), fn(Builder $query, $topic) => $query->where('topic', $topic))
            ->when($request->query('popularity'), fn(Builder $query) => $query->having('comments_count', '>', 10))
            ->get();
    }
}

Вот и все, теперь мы можем фильтровать наши результаты,и мы даже сделали это красиво!

Рейтинг
( 3 оценки, среднее 4.33 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.